library ColorUtils {
    /// a struct to hold vars within the main color scheme
    /// which can be used for both highlight (hlt) an primar (pri) colors
    struct SubScheme {
        int256[3] colA; // either the entire solid color, or one side of the gradient
        int256[3] colB; // either the same as A (solid), or different (gradient)
        bool isInnerGradient; // whether the gradient spans the triangle (true) or canvas (false)
        int256 dirCode; // which direction should the gradient be interpolated
        int256[3] jiggle; // how much to randomly jiffle the color space
        bool isJiggleInner; // does each inner vertiex get a jiggle, or is it triangle wide
        int256[3] backShift; // how much to take off the back face colors
    }

    /// a struct for each piece's color scheme
    struct ColScheme {
        string name;
        uint256 id;
        /// the primary color
        SubScheme pri;
        /// the highlight color
        SubScheme hlt;
        /// remaining parameters (not common to hlt and pri)
        uint256 hltNum;
        int256 hltSelCode;
        int256 hltVarCode;
        /// other scene colors
        int256[3] lightCol;
        int256[3] bgColTop;
        int256[3] bgColBottom;
    }

    /** @dev calculate the color of a prism
    returns an array of 6 colors (for each vertex of a prism) 
     */
    function getColForPrism(
        bytes32 tokenHash,
        int256[3][3] memory triFront,
        SubScheme memory subScheme,
        int256[3][2] memory extents
    ) external view returns (int256[3][6] memory cols) {
        if (
            subScheme.colA[0] == subScheme.colB[0] &&
            subScheme.colA[1] == subScheme.colB[1] &&
            subScheme.colA[2] == subScheme.colB[2]
        ) {
            /// just use color A (as B is the same, so there's no gradient)
            for (uint256 i = 0; i < 6; i++) {
                cols[i] = copyColor(subScheme.colA);
            }
        } else {
            /// get the colors according to the direction code
            int256[3][3] memory triFrontCopy = GeomUtils.copyTri(triFront);
            int256[3][3] memory frontTriCols = applyDirHelp(
                triFrontCopy,
                subScheme.colA,
                subScheme.colB,
                subScheme.dirCode,
                subScheme.isInnerGradient,
                extents
            );

            /// write in the same front colors as the back colors
            for (uint256 i = 0; i < 3; i++) {
                cols[i] = copyColor(frontTriCols[i]);
                cols[i + 3] = copyColor(frontTriCols[i]);
            }
        }

        /// perform the jiggling
        int256[3] memory jiggle;

        if (!subScheme.isJiggleInner) {
            /// get one set of jiggle values to use for all colors created
            jiggle = getJiggle(subScheme.jiggle, tokenHash, 0);
        }

        for (uint256 i = 0; i < 6; i++) {
            if (subScheme.isJiggleInner) {
                // jiggle again per col to create
                // use the last jiggle res in the random seed to get diff jiggles for each prism
                jiggle = getJiggle(subScheme.jiggle, tokenHash, jiggle[0]);
            }

            /// convert to hsv prior to jiggle
            int256[3] memory colHsv = rgb2hsv(
                cols[i][0],
                cols[i][1],
                cols[i][2]
            );

            /// add the jiggle to the colors in hsv space
            colHsv[0] = colHsv[0] + jiggle[0];
            colHsv[1] = colHsv[1] + jiggle[1];
            colHsv[2] = colHsv[2] + jiggle[2];

            /// convert back to rgb
            int256[3] memory colRgb = hsv2rgb(colHsv[0], colHsv[1], colHsv[2]);
            cols[i][0] = colRgb[0];
            cols[i][1] = colRgb[1];
            cols[i][2] = colRgb[2];
        }

        /// perform back shifting
        for (uint256 i = 0; i < 3; i++) {
            cols[i][0] -= subScheme.backShift[0];
            cols[i][1] -= subScheme.backShift[1];
            cols[i][2] -= subScheme.backShift[2];
        }

        /// ensure that we're in 255 range
        for (uint256 i = 0; i < 6; i++) {
            cols[i][0] = ShackledMath.max(0, ShackledMath.min(255, cols[i][0]));
            cols[i][1] = ShackledMath.max(0, ShackledMath.min(255, cols[i][1]));
            cols[i][2] = ShackledMath.max(0, ShackledMath.min(255, cols[i][2]));
        }

        return cols;
    }

    /** @dev roll a schemeId given a list of weightings */
    function getSchemeId(bytes32 tokenHash, int256[2][10] memory weightings)
        internal
        view
        returns (uint256)
    {
        int256 n = GeomUtils.randN(
            tokenHash,
            "schemedId",
            weightings[0][0],
            weightings[weightings.length - 1][1]
        );
        for (uint256 i = 0; i < weightings.length; i++) {
            if (weightings[i][0] <= n && n <= weightings[i][1]) {
                return i;
            }
        }
    }

    /** @dev make a copy of a color */
    function copyColor(int256[3] memory c)
        internal
        view
        returns (int256[3] memory)
    {
        return [c[0], c[1], c[2]];
    }

    /** @dev get a color scheme */
    function getScheme(bytes32 tokenHash, int256[3][3][] memory tris)
        external
        view
        returns (ColScheme memory colScheme)
    {
        /// 'randomly' select 1 of the 9 schemes
        uint256 schemeId = getSchemeId(
            tokenHash,
            [
                [int256(0), 1500],
                [int256(1500), 2500],
                [int256(2500), 3000],
                [int256(3000), 3100],
                [int256(3100), 5500],
                [int256(5500), 6000],
                [int256(6000), 6500],
                [int256(6500), 8000],
                [int256(8000), 9500],
                [int256(9500), 10000]
            ]
        );

        // int256 schemeId = GeomUtils.randN(tokenHash, "schemeID", 1, 9);

        /// define the color scheme to use for this piece
        /// all arrays are on the order of 1000 to remain accurate as integers
        /// will require division by 1000 later when in use

        if (schemeId == 0) {
            /// plain / beigey with a highlight, and a matching background colour
            colScheme = ColScheme({
                name: "Accentuated",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(60), 30, 25],
                    colB: [int256(205), 205, 205],
                    isInnerGradient: false,
                    dirCode: 0,
                    jiggle: [int256(13), 13, 13],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hlt: SubScheme({
                    colA: [int256(255), 0, 0],
                    colB: [int256(255), 50, 0],
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "hltDir", 0, 3), /// get a 'random' dir code
                    jiggle: [int256(50), 50, 50],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hltNum: uint256(GeomUtils.randN(tokenHash, "hltNum", 3, 5)), /// get a 'random' number of highlights between 3 and 5
                hltSelCode: 1, /// 'biggest' selection code
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(0), 0, 0],
                bgColBottom: [int256(1), 1, 1]
            });
        } else if (schemeId == 1) {
            /// neutral overall
            colScheme = ColScheme({
                name: "Emergent",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(0), 77, 255],
                    colB: [int256(0), 255, 25],
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "priDir", 2, 3), /// get a 'random' dir code (2 or 3)
                    jiggle: [int256(60), 60, 60],
                    isJiggleInner: false,
                    backShift: [int256(-255), -255, -255]
                }),
                hlt: SubScheme({
                    colA: [int256(0), 77, 255],
                    colB: [int256(0), 255, 25],
                    isInnerGradient: true,
                    dirCode: 3,
                    jiggle: [int256(60), 60, 60],
                    isJiggleInner: false,
                    backShift: [int256(-255), -255, -255]
                }),
                hltNum: uint256(GeomUtils.randN(tokenHash, "hltNum", 4, 6)), /// get a 'random' number of highlights between 4 and 6
                hltSelCode: 2, /// smallest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(255), 255, 255],
                bgColBottom: [int256(255), 255, 255]
            });
        } else if (schemeId == 2) {
            /// vaporwave
            int256 maxHighlights = ShackledMath.max(0, int256(tris.length) - 8);
            int256 minHighlights = ShackledMath.max(
                0,
                int256(maxHighlights) - 2
            );
            colScheme = ColScheme({
                name: "Sunset",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(179), 0, 179],
                    colB: [int256(0), 0, 255],
                    isInnerGradient: false,
                    dirCode: 2, /// up-down
                    jiggle: [int256(25), 25, 25],
                    isJiggleInner: true,
                    backShift: [int256(127), 127, 127]
                }),
                hlt: SubScheme({
                    colA: [int256(0), 0, 0],
                    colB: [int256(0), 0, 0],
                    isInnerGradient: true,
                    dirCode: 3, /// down-up
                    jiggle: [int256(15), 0, 15],
                    isJiggleInner: true,
                    backShift: [int256(0), 0, 0]
                }),
                hltNum: uint256(
                    GeomUtils.randN(
                        tokenHash,
                        "hltNum",
                        minHighlights,
                        maxHighlights
                    )
                ), /// get a 'random' number of highlights between minHighlights and maxHighlights
                hltSelCode: 2, /// smallest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(250), 103, 247],
                bgColBottom: [int256(157), 104, 250]
            });
        } else if (schemeId == 3) {
            /// gold
            int256 priDirCode = GeomUtils.randN(tokenHash, "pirDir", 0, 1); /// get a 'random' dir code (0 or 1)
            colScheme = ColScheme({
                name: "Stone & Gold",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(50), 50, 50],
                    colB: [int256(100), 100, 100],
                    isInnerGradient: true,
                    dirCode: priDirCode,
                    jiggle: [int256(10), 10, 10],
                    isJiggleInner: true,
                    backShift: [int256(128), 128, 128]
                }),
                hlt: SubScheme({
                    colA: [int256(255), 197, 0],
                    colB: [int256(255), 126, 0],
                    isInnerGradient: true,
                    dirCode: priDirCode,
                    jiggle: [int256(0), 0, 0],
                    isJiggleInner: false,
                    backShift: [int256(64), 64, 64]
                }),
                hltNum: 1,
                hltSelCode: 1, /// biggest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(0), 0, 0],
                bgColBottom: [int256(0), 0, 0]
            });
        } else if (schemeId == 4) {
            /// random pastel colors (sometimes black)
            /// for primary colors,
            /// follow the pattern of making a new and unique seedHash for each variable
            /// so they are independant
            /// seed modifiers = pri/hlt + a/b + /r/g/b
            colScheme = ColScheme({
                name: "Denatured",
                id: schemeId,
                pri: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "PAR", 25, 255),
                        GeomUtils.randN(tokenHash, "PAG", 25, 255),
                        GeomUtils.randN(tokenHash, "PAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "PBR", 25, 255),
                        GeomUtils.randN(tokenHash, "PBG", 25, 255),
                        GeomUtils.randN(tokenHash, "PBB", 25, 255)
                    ],
                    isInnerGradient: false,
                    dirCode: GeomUtils.randN(tokenHash, "pri", 0, 1), /// get a 'random' dir code (0 or 1)
                    jiggle: [int256(0), 0, 0],
                    isJiggleInner: false,
                    backShift: [int256(127), 127, 127]
                }),
                hlt: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "HAR", 25, 255),
                        GeomUtils.randN(tokenHash, "HAG", 25, 255),
                        GeomUtils.randN(tokenHash, "HAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "HBR", 25, 255),
                        GeomUtils.randN(tokenHash, "HBG", 25, 255),
                        GeomUtils.randN(tokenHash, "HBB", 25, 255)
                    ],
                    isInnerGradient: false,
                    dirCode: GeomUtils.randN(tokenHash, "hlt", 0, 1), /// get a 'random' dir code (0 or 1)
                    jiggle: [int256(0), 0, 0],
                    isJiggleInner: false,
                    backShift: [int256(127), 127, 127]
                }),
                hltNum: tris.length / 2,
                hltSelCode: 2, /// smallest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(3), 3, 3],
                bgColBottom: [int256(0), 0, 0]
            });
        } else if (schemeId == 5) {
            /// inter triangle random colors ('chameleonic')

            /// pri dir code is anything (0, 1, 2, 3)
            /// hlt dir code is oppose to pri dir code (rl <-> lr, up <-> du)
            int256 priDirCode = GeomUtils.randN(tokenHash, "pri", 0, 3); /// get a 'random' dir code (0 or 1)
            int256 hltDirCode;
            if (priDirCode == 0 || priDirCode == 1) {
                hltDirCode = priDirCode == 0 ? int256(1) : int256(0);
            } else {
                hltDirCode = priDirCode == 2 ? int256(3) : int256(2);
            }
            /// for primary colors,
            /// follow the pattern of making a new and unique seedHash for each variable
            /// so they are independant
            /// seed modifiers = pri/hlt + a/b + /r/g/b
            colScheme = ColScheme({
                name: "Chameleonic",
                id: schemeId,
                pri: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "PAR", 25, 255),
                        GeomUtils.randN(tokenHash, "PAG", 25, 255),
                        GeomUtils.randN(tokenHash, "PAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "PBR", 25, 255),
                        GeomUtils.randN(tokenHash, "PBG", 25, 255),
                        GeomUtils.randN(tokenHash, "PBB", 25, 255)
                    ],
                    isInnerGradient: true,
                    dirCode: priDirCode,
                    jiggle: [int256(25), 25, 25],
                    isJiggleInner: true,
                    backShift: [int256(0), 0, 0]
                }),
                hlt: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "HAR", 25, 255),
                        GeomUtils.randN(tokenHash, "HAG", 25, 255),
                        GeomUtils.randN(tokenHash, "HAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "HBR", 25, 255),
                        GeomUtils.randN(tokenHash, "HBG", 25, 255),
                        GeomUtils.randN(tokenHash, "HBB", 25, 255)
                    ],
                    isInnerGradient: true,
                    dirCode: hltDirCode,
                    jiggle: [int256(255), 255, 255],
                    isJiggleInner: true,
                    backShift: [int256(205), 205, 205]
                }),
                hltNum: 12,
                hltSelCode: 2, /// smallest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(3), 3, 3],
                bgColBottom: [int256(0), 0, 0]
            });
        } else if (schemeId == 6) {
            /// each prism is a different colour with some randomisation

            /// pri dir code is anything (0, 1, 2, 3)
            /// hlt dir code is oppose to pri dir code (rl <-> lr, up <-> du)
            int256 priDirCode = GeomUtils.randN(tokenHash, "pri", 0, 1); /// get a 'random' dir code (0 or 1)
            int256 hltDirCode;
            if (priDirCode == 0 || priDirCode == 1) {
                hltDirCode = priDirCode == 0 ? int256(1) : int256(0);
            } else {
                hltDirCode = priDirCode == 2 ? int256(3) : int256(2);
            }
            /// for primary colors,
            /// follow the pattern of making a new and unique seedHash for each variable
            /// so they are independant
            /// seed modifiers = pri/hlt + a/b + /r/g/b
            colScheme = ColScheme({
                name: "Gradiated",
                id: schemeId,
                pri: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "PAR", 25, 255),
                        GeomUtils.randN(tokenHash, "PAG", 25, 255),
                        GeomUtils.randN(tokenHash, "PAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "PBR", 25, 255),
                        GeomUtils.randN(tokenHash, "PBG", 25, 255),
                        GeomUtils.randN(tokenHash, "PBB", 25, 255)
                    ],
                    isInnerGradient: false,
                    dirCode: priDirCode,
                    jiggle: [int256(127), 127, 127],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hlt: SubScheme({
                    colA: [
                        GeomUtils.randN(tokenHash, "HAR", 25, 255),
                        GeomUtils.randN(tokenHash, "HAG", 25, 255),
                        GeomUtils.randN(tokenHash, "HAB", 25, 255)
                    ],
                    colB: [
                        GeomUtils.randN(tokenHash, "HBR", 25, 255),
                        GeomUtils.randN(tokenHash, "HBG", 25, 255),
                        GeomUtils.randN(tokenHash, "HBB", 25, 255)
                    ],
                    isInnerGradient: false,
                    dirCode: hltDirCode,
                    jiggle: [int256(127), 127, 127],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hltNum: 12, /// get a 'random' number of highlights between 4 and 6
                hltSelCode: 2, /// smallest-first
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(3), 3, 3],
                bgColBottom: [int256(0), 0, 0]
            });
        } else if (schemeId == 7) {
            /// feature colour on white primary, with feature colour background
            /// calculate the feature color in hsv
            int256[3] memory hsv = [
                GeomUtils.randN(tokenHash, "hsv", 0, 255),
                230,
                255
            ];
            int256[3] memory hltColA = hsv2rgb(hsv[0], hsv[1], hsv[2]);

            colScheme = ColScheme({
                name: "Vivid Alabaster",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(255), 255, 255],
                    colB: [int256(255), 255, 255],
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// get a 'random' dir code (0 or 1)
                    jiggle: [int256(25), 25, 25],
                    isJiggleInner: true,
                    backShift: [int256(127), 127, 127]
                }),
                hlt: SubScheme({
                    colA: hltColA,
                    colB: copyColor(hltColA), /// same as A
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// same as priDirCode
                    jiggle: [int256(25), 50, 50],
                    isJiggleInner: true,
                    backShift: [int256(180), 180, 180]
                }),
                hltNum: tris.length % 2 == 1
                    ? (tris.length / 2) + 1
                    : tris.length / 2,
                hltSelCode: GeomUtils.randN(tokenHash, "hltSel", 0, 2),
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: hsv2rgb(
                    ShackledMath.mod((hsv[0] - 9), 255),
                    105,
                    255
                ),
                bgColBottom: hsv2rgb(
                    ShackledMath.mod((hsv[0] + 9), 255),
                    105,
                    255
                )
            });
        } else if (schemeId == 8) {
            /// feature colour on black primary, with feature colour background
            /// calculate the feature color in hsv
            int256[3] memory hsv = [
                GeomUtils.randN(tokenHash, "hsv", 0, 255),
                245,
                190
            ];

            int256[3] memory hltColA = hsv2rgb(hsv[0], hsv[1], hsv[2]);

            colScheme = ColScheme({
                name: "Vivid Ink",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(0), 0, 0],
                    colB: [int256(0), 0, 0],
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// get a 'random' dir code (0 or 1)
                    jiggle: [int256(25), 25, 25],
                    isJiggleInner: false,
                    backShift: [int256(-60), -60, -60]
                }),
                hlt: SubScheme({
                    colA: hltColA,
                    colB: copyColor(hltColA), /// same as A
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// same as priDirCode
                    jiggle: [int256(0), 0, 0],
                    isJiggleInner: false,
                    backShift: [int256(-60), -60, -60]
                }),
                hltNum: tris.length % 2 == 1
                    ? (tris.length / 2) + 1
                    : tris.length / 2,
                hltSelCode: GeomUtils.randN(tokenHash, "hltSel", 0, 2),
                hltVarCode: GeomUtils.randN(tokenHash, "hltVar", 0, 2),
                lightCol: [int256(255), 255, 255],
                bgColTop: hsv2rgb(
                    ShackledMath.mod((hsv[0] - 9), 255),
                    105,
                    255
                ),
                bgColBottom: hsv2rgb(
                    ShackledMath.mod((hsv[0] + 9), 255),
                    105,
                    255
                )
            });
        } else if (schemeId == 9) {
            colScheme = ColScheme({
                name: "Pigmented",
                id: schemeId,
                pri: SubScheme({
                    colA: [int256(50), 30, 25],
                    colB: [int256(205), 205, 205],
                    isInnerGradient: false,
                    dirCode: 0,
                    jiggle: [int256(13), 13, 13],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hlt: SubScheme({
                    colA: [int256(255), 0, 0],
                    colB: [int256(255), 50, 0],
                    isInnerGradient: true,
                    dirCode: GeomUtils.randN(tokenHash, "hltDir", 0, 3), /// get a 'random' dir code
                    jiggle: [int256(255), 50, 50],
                    isJiggleInner: false,
                    backShift: [int256(205), 205, 205]
                }),
                hltNum: tris.length / 3,
                hltSelCode: 1, /// 'biggest' selection code
                hltVarCode: 0,
                lightCol: [int256(255), 255, 255],
                bgColTop: [int256(0), 0, 0],
                bgColBottom: [int256(7), 7, 7]
            });
        } else {
            revert("invalid scheme id");
        }

        return colScheme;
    }

    /** @dev convert hsv to rgb color
    assume h, s and v and in range [0, 255]
    outputs rgb in range [0, 255]
     */
    function hsv2rgb(
        int256 h,
        int256 s,
        int256 v
    ) internal view returns (int256[3] memory res) {
        /// ensure range 0, 255
        h = ShackledMath.max(0, ShackledMath.min(255, h));
        s = ShackledMath.max(0, ShackledMath.min(255, s));
        v = ShackledMath.max(0, ShackledMath.min(255, v));

        int256 h2 = (((h % 255) * 1e3) / 255) * 360; /// convert to degress
        int256 v2 = (v * 1e3) / 255;
        int256 s2 = (s * 1e3) / 255;

        /// calculate c, x and m while scaling all by 1e3
        /// otherwise x will be too small and round to 0
        int256 c = (v2 * s2) / 1e3;

        int256 x = (c *
            (1 * 1e3 - ShackledMath.abs(((h2 / 60) % (2 * 1e3)) - (1 * 1e3))));

        x = x / 1e3;

        int256 m = v2 - c;

        if (0 <= h2 && h2 < 60000) {
            res = [c + m, x + m, m];
        } else if (60000 <= h2 && h2 < 120000) {
            res = [x + m, c + m, m];
        } else if (120000 < h2 && h2 < 180000) {
            res = [m, c + m, x + m];
        } else if (180000 < h2 && h2 < 240000) {
            res = [m, x + m, c + m];
        } else if (240000 < h2 && h2 < 300000) {
            res = [x + m, m, c + m];
        } else if (300000 < h2 && h2 < 360000) {
            res = [c + m, m, x + m];
        } else {
            res = [int256(0), 0, 0];
        }

        /// scale into correct range
        return [
            (res[0] * 255) / 1e3,
            (res[1] * 255) / 1e3,
            (res[2] * 255) / 1e3
        ];
    }

    /** @dev convert rgb to hsv 
        expects rgb to be in range [0, 255]
        outputs hsv in range [0, 255]
    */
    function rgb2hsv(
        int256 r,
        int256 g,
        int256 b
    ) internal view returns (int256[3] memory) {
        int256 r2 = (r * 1e3) / 255;
        int256 g2 = (g * 1e3) / 255;
        int256 b2 = (b * 1e3) / 255;
        int256 max = ShackledMath.max(ShackledMath.max(r2, g2), b2);
        int256 min = ShackledMath.min(ShackledMath.min(r2, g2), b2);
        int256 delta = max - min;

        /// calculate hue
        int256 h;
        if (delta != 0) {
            if (max == r2) {
                int256 _h = ((g2 - b2) * 1e3) / delta;
                h = 60 * ShackledMath.mod(_h, 6000);
            } else if (max == g2) {
                h = 60 * (((b2 - r2) * 1e3) / delta + (2000));
            } else if (max == b2) {
                h = 60 * (((r2 - g2) * 1e3) / delta + (4000));
            }
        }

        h = (h % (360 * 1e3)) / 360;

        /// calculate saturation
        int256 s;
        if (max != 0) {
            s = (delta * 1e3) / max;
        }

        /// calculate value
        int256 v = max;

        return [(h * 255) / 1e3, (s * 255) / 1e3, (v * 255) / 1e3];
    }

    /** @dev get vector of three numbers that can be used to jiggle a color */
    function getJiggle(
        int256[3] memory jiggle,
        bytes32 randomSeed,
        int256 seedModifier
    ) internal view returns (int256[3] memory) {
        return [
            jiggle[0] +
                GeomUtils.randN(
                    randomSeed,
                    string(abi.encodePacked("0", seedModifier)),
                    -jiggle[0],
                    jiggle[0]
                ),
            jiggle[1] +
                GeomUtils.randN(
                    randomSeed,
                    string(abi.encodePacked("1", seedModifier)),
                    -jiggle[1],
                    jiggle[1]
                ),
            jiggle[2] +
                GeomUtils.randN(
                    randomSeed,
                    string(abi.encodePacked("2", seedModifier)),
                    -jiggle[2],
                    jiggle[2]
                )
        ];
    }

    /** @dev check if a uint is in an array */
    function inArray(uint256[] memory array, uint256 value)
        external
        view
        returns (bool)
    {
        for (uint256 i = 0; i < array.length; i++) {
            if (array[i] == value) {
                return true;
            }
        }
        return false;
    }

    /** @dev a helper function to apply the direction code in interpolation */
    function applyDirHelp(
        int256[3][3] memory triFront,
        int256[3] memory colA,
        int256[3] memory colB,
        int256 dirCode,
        bool isInnerGradient,
        int256[3][2] memory extents
    ) internal view returns (int256[3][3] memory triCols) {
        uint256[3] memory order;
        if (isInnerGradient) {
            /// perform the simple 3 sort - always color by the front
            order = getOrderedPointIdxsInDir(triFront, dirCode);
        } else {
            /// order irrelevant in other case
            order = [uint256(0), 1, 2];
        }

        /// axis is 0 (horizontal) if dir code is left-right or right-left
        /// 1 (vertical) otherwise
        uint256 axis = (dirCode == 0 || dirCode == 1) ? 0 : 1;

        int256 length;
        if (axis == 0) {
            length = extents[1][0] - extents[0][0];
        } else {
            length = extents[1][1] - extents[0][1];
        }

        /// if we're interpolating across the triangle (inner)
        /// then do so by calculating the color at each point in the triangle
        for (uint256 i = 0; i < 3; i++) {
            triCols[order[i]] = interpColHelp(
                colA,
                colB,
                (isInnerGradient)
                    ? triFront[order[0]][axis]
                    : int256(-length / 2),
                (isInnerGradient)
                    ? triFront[order[2]][axis]
                    : int256(length / 2),
                triFront[order[i]][axis]
            );
        }
    }

    /** @dev a helper function to order points by index in a desired direction
     */
    function getOrderedPointIdxsInDir(int256[3][3] memory tri, int256 dirCode)
        internal
        view
        returns (uint256[3] memory)
    {
        // flip if dir is left-right or down-up
        bool flip = (dirCode == 1 || dirCode == 3) ? true : false;

        // axis is 0 if horizontal (left-right or right-left), 1 otherwise (vertical)
        uint256 axis = (dirCode == 0 || dirCode == 1) ? 0 : 1;

        /// get the values of each point in the tri (flipped as required)
        int256 f = (flip) ? int256(-1) : int256(1);
        int256 a = f * tri[0][axis];
        int256 b = f * tri[1][axis];
        int256 c = f * tri[2][axis];

        /// get the ordered indices
        uint256[3] memory ixOrd = [uint256(0), 1, 2];

        /// simplest way to sort 3 numbers
        if (a > b) {
            (a, b) = (b, a);
            (ixOrd[0], ixOrd[1]) = (ixOrd[1], ixOrd[0]);
        }
        if (a > c) {
            (a, c) = (c, a);
            (ixOrd[0], ixOrd[2]) = (ixOrd[2], ixOrd[0]);
        }
        if (b > c) {
            (b, c) = (c, b);
            (ixOrd[1], ixOrd[2]) = (ixOrd[2], ixOrd[1]);
        }
        return ixOrd;
    }

    /** @dev a helper function for linear interpolation betweet two colors*/
    function interpColHelp(
        int256[3] memory colA,
        int256[3] memory colB,
        int256 low,
        int256 high,
        int256 val
    ) internal view returns (int256[3] memory result) {
        int256 ir;
        int256 lerpScaleFactor = 1e3;
        if (high - low == 0) {
            ir = 1;
        } else {
            ir = ((val - low) * lerpScaleFactor) / (high - low);
        }

        for (uint256 i = 0; i < 3; i++) {
            /// dont allow interpolation to go below 0
            result[i] = ShackledMath.max(
                0,
                colA[i] + ((colB[i] - colA[i]) * ir) / lerpScaleFactor
            );
        }
    }

    /** @dev get indexes of the prisms to use highlight coloring*/
    function getHighlightPrismIdxs(
        int256[3][3][] memory tris,
        bytes32 tokenHash,
        uint256 nHighlights,
        int256 varCode,
        int256 selCode
    ) internal view returns (uint256[] memory idxs) {
        nHighlights = nHighlights < tris.length ? nHighlights : tris.length;

        ///if we just want random triangles then there's no need to sort
        if (selCode == 0) {
            idxs = ShackledMath.randomIdx(
                tokenHash,
                uint256(nHighlights),
                tris.length - 1
            );
        } else {
            idxs = getSortedTrisIdxs(tris, nHighlights, varCode, selCode);
        }
    }

    /** @dev return the index of the tris sorted by sel code
    @param selCode will be 1 (biggest first) or 2 (smallest first)
    */
    function getSortedTrisIdxs(
        int256[3][3][] memory tris,
        uint256 nHighlights,
        int256 varCode,
        int256 selCode
    ) internal view returns (uint256[] memory) {
        // determine the sort order
        int256 orderFactor = (selCode == 2) ? int256(1) : int256(-1);
        /// get the list of triangle sizes
        int256[] memory sizes = new int256[](tris.length);
        for (uint256 i = 0; i < tris.length; i++) {
            if (varCode == 0) {
                // use size
                sizes[i] = GeomUtils.getRadiusLen(tris[i]) * orderFactor;
            } else if (varCode == 1) {
                // use x
                sizes[i] = GeomUtils.getCenterVec(tris[i])[0] * orderFactor;
            } else if (varCode == 2) {
                // use y
                sizes[i] = GeomUtils.getCenterVec(tris[i])[1] * orderFactor;
            }
        }
        /// initialise the index array
        uint256[] memory idxs = new uint256[](tris.length);
        for (uint256 i = 0; i < tris.length; i++) {
            idxs[i] = i;
        }
        /// run a boilerplate insertion sort over the index array
        for (uint256 i = 1; i < tris.length; i++) {
            int256 key = sizes[i];
            uint256 j = i - 1;
            while (j > 0 && key < sizes[j]) {
                sizes[j + 1] = sizes[j];
                idxs[j + 1] = idxs[j];
                j--;
            }
            sizes[j + 1] = key;
            idxs[j + 1] = i;
        }

        uint256 nToCull = tris.length - nHighlights;
        assembly {
            mstore(idxs, sub(mload(idxs), nToCull))
        }

        return idxs;
    }
}
/** Hold some functions useful for coloring in the prisms  */
function getScheme(bytes32 tokenHash, int256[3][3][] memory tris)
    external
    view
    returns (ColScheme memory colScheme)
{
    /// 'randomly' select 1 of the 9 schemes
    uint256 schemeId = getSchemeId(
        tokenHash,
        [
            [int256(0), 1500],
            [int256(1500), 2500],
            [int256(2500), 3000],
            [int256(3000), 3100],
            [int256(3100), 5500],
            [int256(5500), 6000],
            [int256(6000), 6500],
            [int256(6500), 8000],
            [int256(8000), 9500],
            [int256(9500), 10000]
        ]
    );

    // int256 schemeId = GeomUtils.randN(tokenHash, "schemeID", 1, 9);

    /// define the color scheme to use for this piece
    /// all arrays are on the order of 1000 to remain accurate as integers
    /// will require division by 1000 later when in use

    if (schemeId == 0) {
        /// plain / beigey with a highlight, and a matching background colour
        colScheme = ColScheme({
            name: "Accentuated",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(60), 30, 25],
                colB: [int256(205), 205, 205],
                isInnerGradient: false,
                dirCode: 0,
                jiggle: [int256(13), 13, 13],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hlt: SubScheme({
                colA: [int256(255), 0, 0],
                colB: [int256(255), 50, 0],
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "hltDir", 0, 3), /// get a 'random' dir code
                jiggle: [int256(50), 50, 50],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hltNum: uint256(GeomUtils.randN(tokenHash, "hltNum", 3, 5)), /// get a 'random' number of highlights between 3 and 5
            hltSelCode: 1, /// 'biggest' selection code
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(0), 0, 0],
            bgColBottom: [int256(1), 1, 1]
        });
    } else if (schemeId == 1) {
        /// neutral overall
        colScheme = ColScheme({
            name: "Emergent",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(0), 77, 255],
                colB: [int256(0), 255, 25],
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "priDir", 2, 3), /// get a 'random' dir code (2 or 3)
                jiggle: [int256(60), 60, 60],
                isJiggleInner: false,
                backShift: [int256(-255), -255, -255]
            }),
            hlt: SubScheme({
                colA: [int256(0), 77, 255],
                colB: [int256(0), 255, 25],
                isInnerGradient: true,
                dirCode: 3,
                jiggle: [int256(60), 60, 60],
                isJiggleInner: false,
                backShift: [int256(-255), -255, -255]
            }),
            hltNum: uint256(GeomUtils.randN(tokenHash, "hltNum", 4, 6)), /// get a 'random' number of highlights between 4 and 6
            hltSelCode: 2, /// smallest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(255), 255, 255],
            bgColBottom: [int256(255), 255, 255]
        });
    } else if (schemeId == 2) {
        /// vaporwave
        int256 maxHighlights = ShackledMath.max(0, int256(tris.length) - 8);
        int256 minHighlights = ShackledMath.max(
            0,
            int256(maxHighlights) - 2
        );
        colScheme = ColScheme({
            name: "Sunset",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(179), 0, 179],
                colB: [int256(0), 0, 255],
                isInnerGradient: false,
                dirCode: 2, /// up-down
                jiggle: [int256(25), 25, 25],
                isJiggleInner: true,
                backShift: [int256(127), 127, 127]
            }),
            hlt: SubScheme({
                colA: [int256(0), 0, 0],
                colB: [int256(0), 0, 0],
                isInnerGradient: true,
                dirCode: 3, /// down-up
                jiggle: [int256(15), 0, 15],
                isJiggleInner: true,
                backShift: [int256(0), 0, 0]
            }),
            hltNum: uint256(
                GeomUtils.randN(
                    tokenHash,
                    "hltNum",
                    minHighlights,
                    maxHighlights
                )
            ), /// get a 'random' number of highlights between minHighlights and maxHighlights
            hltSelCode: 2, /// smallest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(250), 103, 247],
            bgColBottom: [int256(157), 104, 250]
        });
    } else if (schemeId == 3) {
        /// gold
        int256 priDirCode = GeomUtils.randN(tokenHash, "pirDir", 0, 1); /// get a 'random' dir code (0 or 1)
        colScheme = ColScheme({
            name: "Stone & Gold",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(50), 50, 50],
                colB: [int256(100), 100, 100],
                isInnerGradient: true,
                dirCode: priDirCode,
                jiggle: [int256(10), 10, 10],
                isJiggleInner: true,
                backShift: [int256(128), 128, 128]
            }),
            hlt: SubScheme({
                colA: [int256(255), 197, 0],
                colB: [int256(255), 126, 0],
                isInnerGradient: true,
                dirCode: priDirCode,
                jiggle: [int256(0), 0, 0],
                isJiggleInner: false,
                backShift: [int256(64), 64, 64]
            }),
            hltNum: 1,
            hltSelCode: 1, /// biggest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(0), 0, 0],
            bgColBottom: [int256(0), 0, 0]
        });
    } else if (schemeId == 4) {
        /// random pastel colors (sometimes black)
        /// for primary colors,
        /// follow the pattern of making a new and unique seedHash for each variable
        /// so they are independant
        /// seed modifiers = pri/hlt + a/b + /r/g/b
        colScheme = ColScheme({
            name: "Denatured",
            id: schemeId,
            pri: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "PAR", 25, 255),
                    GeomUtils.randN(tokenHash, "PAG", 25, 255),
                    GeomUtils.randN(tokenHash, "PAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "PBR", 25, 255),
                    GeomUtils.randN(tokenHash, "PBG", 25, 255),
                    GeomUtils.randN(tokenHash, "PBB", 25, 255)
                ],
                isInnerGradient: false,
                dirCode: GeomUtils.randN(tokenHash, "pri", 0, 1), /// get a 'random' dir code (0 or 1)
                jiggle: [int256(0), 0, 0],
                isJiggleInner: false,
                backShift: [int256(127), 127, 127]
            }),
            hlt: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "HAR", 25, 255),
                    GeomUtils.randN(tokenHash, "HAG", 25, 255),
                    GeomUtils.randN(tokenHash, "HAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "HBR", 25, 255),
                    GeomUtils.randN(tokenHash, "HBG", 25, 255),
                    GeomUtils.randN(tokenHash, "HBB", 25, 255)
                ],
                isInnerGradient: false,
                dirCode: GeomUtils.randN(tokenHash, "hlt", 0, 1), /// get a 'random' dir code (0 or 1)
                jiggle: [int256(0), 0, 0],
                isJiggleInner: false,
                backShift: [int256(127), 127, 127]
            }),
            hltNum: tris.length / 2,
            hltSelCode: 2, /// smallest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(3), 3, 3],
            bgColBottom: [int256(0), 0, 0]
        });
    } else if (schemeId == 5) {
        /// inter triangle random colors ('chameleonic')

        /// pri dir code is anything (0, 1, 2, 3)
        /// hlt dir code is oppose to pri dir code (rl <-> lr, up <-> du)
        int256 priDirCode = GeomUtils.randN(tokenHash, "pri", 0, 3); /// get a 'random' dir code (0 or 1)
        int256 hltDirCode;
        if (priDirCode == 0 || priDirCode == 1) {
            hltDirCode = priDirCode == 0 ? int256(1) : int256(0);
        } else {
            hltDirCode = priDirCode == 2 ? int256(3) : int256(2);
        }
        /// for primary colors,
        /// follow the pattern of making a new and unique seedHash for each variable
        /// so they are independant
        /// seed modifiers = pri/hlt + a/b + /r/g/b
        colScheme = ColScheme({
            name: "Chameleonic",
            id: schemeId,
            pri: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "PAR", 25, 255),
                    GeomUtils.randN(tokenHash, "PAG", 25, 255),
                    GeomUtils.randN(tokenHash, "PAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "PBR", 25, 255),
                    GeomUtils.randN(tokenHash, "PBG", 25, 255),
                    GeomUtils.randN(tokenHash, "PBB", 25, 255)
                ],
                isInnerGradient: true,
                dirCode: priDirCode,
                jiggle: [int256(25), 25, 25],
                isJiggleInner: true,
                backShift: [int256(0), 0, 0]
            }),
            hlt: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "HAR", 25, 255),
                    GeomUtils.randN(tokenHash, "HAG", 25, 255),
                    GeomUtils.randN(tokenHash, "HAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "HBR", 25, 255),
                    GeomUtils.randN(tokenHash, "HBG", 25, 255),
                    GeomUtils.randN(tokenHash, "HBB", 25, 255)
                ],
                isInnerGradient: true,
                dirCode: hltDirCode,
                jiggle: [int256(255), 255, 255],
                isJiggleInner: true,
                backShift: [int256(205), 205, 205]
            }),
            hltNum: 12,
            hltSelCode: 2, /// smallest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(3), 3, 3],
            bgColBottom: [int256(0), 0, 0]
        });
    } else if (schemeId == 6) {
        /// each prism is a different colour with some randomisation

        /// pri dir code is anything (0, 1, 2, 3)
        /// hlt dir code is oppose to pri dir code (rl <-> lr, up <-> du)
        int256 priDirCode = GeomUtils.randN(tokenHash, "pri", 0, 1); /// get a 'random' dir code (0 or 1)
        int256 hltDirCode;
        if (priDirCode == 0 || priDirCode == 1) {
            hltDirCode = priDirCode == 0 ? int256(1) : int256(0);
        } else {
            hltDirCode = priDirCode == 2 ? int256(3) : int256(2);
        }
        /// for primary colors,
        /// follow the pattern of making a new and unique seedHash for each variable
        /// so they are independant
        /// seed modifiers = pri/hlt + a/b + /r/g/b
        colScheme = ColScheme({
            name: "Gradiated",
            id: schemeId,
            pri: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "PAR", 25, 255),
                    GeomUtils.randN(tokenHash, "PAG", 25, 255),
                    GeomUtils.randN(tokenHash, "PAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "PBR", 25, 255),
                    GeomUtils.randN(tokenHash, "PBG", 25, 255),
                    GeomUtils.randN(tokenHash, "PBB", 25, 255)
                ],
                isInnerGradient: false,
                dirCode: priDirCode,
                jiggle: [int256(127), 127, 127],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hlt: SubScheme({
                colA: [
                    GeomUtils.randN(tokenHash, "HAR", 25, 255),
                    GeomUtils.randN(tokenHash, "HAG", 25, 255),
                    GeomUtils.randN(tokenHash, "HAB", 25, 255)
                ],
                colB: [
                    GeomUtils.randN(tokenHash, "HBR", 25, 255),
                    GeomUtils.randN(tokenHash, "HBG", 25, 255),
                    GeomUtils.randN(tokenHash, "HBB", 25, 255)
                ],
                isInnerGradient: false,
                dirCode: hltDirCode,
                jiggle: [int256(127), 127, 127],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hltNum: 12, /// get a 'random' number of highlights between 4 and 6
            hltSelCode: 2, /// smallest-first
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(3), 3, 3],
            bgColBottom: [int256(0), 0, 0]
        });
    } else if (schemeId == 7) {
        /// feature colour on white primary, with feature colour background
        /// calculate the feature color in hsv
        int256[3] memory hsv = [
            GeomUtils.randN(tokenHash, "hsv", 0, 255),
            230,
            255
        ];
        int256[3] memory hltColA = hsv2rgb(hsv[0], hsv[1], hsv[2]);

        colScheme = ColScheme({
            name: "Vivid Alabaster",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(255), 255, 255],
                colB: [int256(255), 255, 255],
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// get a 'random' dir code (0 or 1)
                jiggle: [int256(25), 25, 25],
                isJiggleInner: true,
                backShift: [int256(127), 127, 127]
            }),
            hlt: SubScheme({
                colA: hltColA,
                colB: copyColor(hltColA), /// same as A
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// same as priDirCode
                jiggle: [int256(25), 50, 50],
                isJiggleInner: true,
                backShift: [int256(180), 180, 180]
            }),
            hltNum: tris.length % 2 == 1
                ? (tris.length / 2) + 1
                : tris.length / 2,
            hltSelCode: GeomUtils.randN(tokenHash, "hltSel", 0, 2),
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: hsv2rgb(
                ShackledMath.mod((hsv[0] - 9), 255),
                105,
                255
            ),
            bgColBottom: hsv2rgb(
                ShackledMath.mod((hsv[0] + 9), 255),
                105,
                255
            )
        });
    } else if (schemeId == 8) {
        /// feature colour on black primary, with feature colour background
        /// calculate the feature color in hsv
        int256[3] memory hsv = [
            GeomUtils.randN(tokenHash, "hsv", 0, 255),
            245,
            190
        ];

        int256[3] memory hltColA = hsv2rgb(hsv[0], hsv[1], hsv[2]);

        colScheme = ColScheme({
            name: "Vivid Ink",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(0), 0, 0],
                colB: [int256(0), 0, 0],
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// get a 'random' dir code (0 or 1)
                jiggle: [int256(25), 25, 25],
                isJiggleInner: false,
                backShift: [int256(-60), -60, -60]
            }),
            hlt: SubScheme({
                colA: hltColA,
                colB: copyColor(hltColA), /// same as A
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "pri", 0, 3), /// same as priDirCode
                jiggle: [int256(0), 0, 0],
                isJiggleInner: false,
                backShift: [int256(-60), -60, -60]
            }),
            hltNum: tris.length % 2 == 1
                ? (tris.length / 2) + 1
                : tris.length / 2,
            hltSelCode: GeomUtils.randN(tokenHash, "hltSel", 0, 2),
            hltVarCode: GeomUtils.randN(tokenHash, "hltVar", 0, 2),
            lightCol: [int256(255), 255, 255],
            bgColTop: hsv2rgb(
                ShackledMath.mod((hsv[0] - 9), 255),
                105,
                255
            ),
            bgColBottom: hsv2rgb(
                ShackledMath.mod((hsv[0] + 9), 255),
                105,
                255
            )
        });
    } else if (schemeId == 9) {
        colScheme = ColScheme({
            name: "Pigmented",
            id: schemeId,
            pri: SubScheme({
                colA: [int256(50), 30, 25],
                colB: [int256(205), 205, 205],
                isInnerGradient: false,
                dirCode: 0,
                jiggle: [int256(13), 13, 13],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hlt: SubScheme({
                colA: [int256(255), 0, 0],
                colB: [int256(255), 50, 0],
                isInnerGradient: true,
                dirCode: GeomUtils.randN(tokenHash, "hltDir", 0, 3), /// get a 'random' dir code
                jiggle: [int256(255), 50, 50],
                isJiggleInner: false,
                backShift: [int256(205), 205, 205]
            }),
            hltNum: tris.length / 3,
            hltSelCode: 1, /// 'biggest' selection code
            hltVarCode: 0,
            lightCol: [int256(255), 255, 255],
            bgColTop: [int256(0), 0, 0],
            bgColBottom: [int256(7), 7, 7]
        });
    } else {
        revert("invalid scheme id");
    }

    return colScheme;
}

/** @dev get a color scheme */