diff --git a/src/extensions/kinemage/kin.ts b/src/extensions/kinemage/kin.ts index e92d1dbcf..29a78afd3 100644 --- a/src/extensions/kinemage/kin.ts +++ b/src/extensions/kinemage/kin.ts @@ -130,9 +130,7 @@ async function getPoints(ctx: RuntimeContext, kin: Kinemage) { let group = index++; builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group); // colorArray may be undefined; push a default color when not provided - colors.push(colorArray && colorArray.length > j * 3 ? - Color.fromRgb(255 * (colorArray[3 * j + 0]), 255 * (colorArray[3 * j + 1]), 255 * (colorArray[3 * j + 2])) - : Color.fromRgb(255, 255, 255)); + colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255)) // labelArray may be undefined; push an empty string when not provided labels.push(labelArray && labelArray.length > j ? labelArray[j] : ''); } @@ -182,15 +180,11 @@ async function getLines(ctx: RuntimeContext, kin: Kinemage) { midX, midY, midZ, group); // widthArray may be undefined; push NaN when width not provided - widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN); + widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN) // colorArray may be undefined; push a default color when not provided - colors.push(color1Array && color1Array.length > j * 3 ? - Color.fromRgb(255 * color1Array[3 * j + 0], - 255 * color1Array[3 * j + 1], - 255 * color1Array[3 * j + 2]) - : Color.fromRgb(255, 255, 255)); + colors.push(color1Array && color1Array.length > j ? color1Array[j] : Color.fromRgb(255, 255, 255)) // labelArray may be undefined; push an empty string when not provided - labels.push(label1Array && label1Array.length > j ? label1Array[j] : ''); + labels.push(label1Array && label1Array.length > j ? label1Array[j] : '') // Make the second half of the line from the midpoint to position2, labeled and colored based on position2. group = index++; @@ -198,15 +192,11 @@ async function getLines(ctx: RuntimeContext, kin: Kinemage) { position2Array[3 * j + 0], position2Array[3 * j + 1], position2Array[3 * j + 2], group); // widthArray may be undefined; push NaN when width not provided - widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN); + widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN) // colorArray may be undefined; push a default color when not provided - colors.push(color2Array && color2Array.length > j * 3 ? - Color.fromRgb(255 * color2Array[3 * j + 0], - 255 * color2Array[3 * j + 1], - 255 * color2Array[3 * j + 2]) - : Color.fromRgb(255, 255, 255)); + colors.push(color2Array && color2Array.length > j ? color2Array[j] : Color.fromRgb(255, 255, 255)) // labelArray may be undefined; push an empty string when not provided - labels.push(label2Array && label2Array.length > j ? label2Array[j] : ''); + labels.push(label2Array && label2Array.length > j ? label2Array[j] : '') } } @@ -267,12 +257,9 @@ async function getMesh(ctx: RuntimeContext, kin: Kinemage) { // colorArray may be undefined; push a default color when not provided. // There is one color per group, even if we have two triangles in this group. - const color = colorArray && colorArray.length > i * 9 ? - Color.fromRgb(255 * colorArray[9 * i + 0], - 255 * colorArray[9 * i + 1], - 255 * colorArray[9 * i + 2]) - : Color.fromRgb(255, 255, 255); + const color = colorArray && colorArray.length > i * 3 ? colorArray[3 * i] : Color.fromRgb(255, 255, 255); colors.push(color); + console.log('XXX ribbon %o color[%o] = %o', ri, group, color) // labelArray may be undefined; push an empty string when not provided const label = labelArray && labelArray.length > i ? labelArray[i] : ''; @@ -337,9 +324,7 @@ async function getSpheres(ctx: RuntimeContext, kin: Kinemage) { // radiusArray may be undefined; push NaN when radius not provided radii.push(radiusArray && radiusArray.length > j ? radiusArray[j] : NaN); // colorArray may be undefined; push a default color when not provided - colors.push(colorArray && colorArray.length > j * 3 ? - Color.fromRgb(255 * (colorArray[3 * j + 0]), 255 * (colorArray[3 * j + 1]), 255 * (colorArray[3 * j + 2])) - : Color.fromRgb(255, 255, 255)); + colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255)); // labelArray may be undefined; push an empty string when not provided labels.push(balls[i].labelArray && balls[i].labelArray.length > j ? balls[i].labelArray[j] : ''); } @@ -374,7 +359,7 @@ function makePointsShapeGetter() { _points, colorFn, // color function reads per-point colors () => 1, // size function - labelFn // label function reads per-point labels + labelFn // label function reads per-point labels ); return _shape; }; diff --git a/src/extensions/kinemage/reader/ngl-based-parser.ts b/src/extensions/kinemage/reader/ngl-based-parser.ts index ad953194b..d29eaebd3 100644 --- a/src/extensions/kinemage/reader/ngl-based-parser.ts +++ b/src/extensions/kinemage/reader/ngl-based-parser.ts @@ -8,7 +8,7 @@ /** * file Kin Parser - * author Alexander Rose + * @author Alexander Rose */ // import { Debug, Log, ParserRegistry } from '../globals' @@ -18,57 +18,38 @@ /// @todo Fill in commments import { Kinemage, RibbonObject } from './schema'; +import { Hsv } from '../../../mol-util/color/spaces/hsv'; +import { Color } from '../../../mol-util/color'; -function hsvToRgb (h: number, s: number, v: number) { - h /= 360 - s /= 100 - v /= 100 - let r, g, b - const i = Math.floor(h * 6) - const f = h * 6 - i - const p = v * (1 - s) - const q = v * (1 - f * s) - const t = v * (1 - (1 - f) * s) - switch (i % 6) { - case 0: r = v; g = t; b = p; break - case 1: r = q; g = v; b = p; break - case 2: r = p; g = v; b = t; break - case 3: r = p; g = q; b = v; break - case 4: r = t; g = p; b = v; break - case 5: r = v; g = p; b = q; break - } - return [ r, g, b ] as number [] -} - -const ColorDict: {[k: string]: number[]} = { - red: hsvToRgb(0, 100, 100), - orange: hsvToRgb(20, 100, 100), - gold: hsvToRgb(40, 100, 100), - yellow: hsvToRgb(60, 100, 100), - lime: hsvToRgb(80, 100, 100), - green: hsvToRgb(120, 80, 100), - sea: hsvToRgb(150, 100, 100), - cyan: hsvToRgb(180, 100, 85), - sky: hsvToRgb(210, 75, 95), - blue: hsvToRgb(240, 70, 100), - purple: hsvToRgb(275, 75, 100), - magenta: hsvToRgb(300, 95, 100), - hotpink: hsvToRgb(335, 100, 100), - pink: hsvToRgb(350, 55, 100), - peach: hsvToRgb(25, 75, 100), - lilac: hsvToRgb(275, 55, 100), - pinktint: hsvToRgb(340, 30, 100), - peachtint: hsvToRgb(25, 50, 100), - yellowtint: hsvToRgb(60, 50, 100), - greentint: hsvToRgb(135, 40, 100), - bluetint: hsvToRgb(220, 40, 100), - lilactint: hsvToRgb(275, 35, 100), - white: hsvToRgb(0, 0, 100), - gray: hsvToRgb(0, 0, 50), - brown: hsvToRgb(20, 45, 75), - deadwhite: [ 1, 1, 1 ], - deadblack: [ 0, 0, 0 ], - invisible: [ 0, 0, 0 ] +const ColorDict: {[k: string]: Color } = { + red: Hsv.toColor(Hsv.fromArray([0, 100, 100])), + orange: Hsv.toColor(Hsv.fromArray([20, 100, 100])), + gold: Hsv.toColor(Hsv.fromArray([40, 100, 100])), + yellow: Hsv.toColor(Hsv.fromArray([60, 100, 100])), + lime: Hsv.toColor(Hsv.fromArray([80, 100, 100])), + green: Hsv.toColor(Hsv.fromArray([120, 80, 100])), + sea: Hsv.toColor(Hsv.fromArray([150, 100, 100])), + cyan: Hsv.toColor(Hsv.fromArray([180, 100, 85])), + sky: Hsv.toColor(Hsv.fromArray([210, 75, 95])), + blue: Hsv.toColor(Hsv.fromArray([240, 70, 100])), + purple: Hsv.toColor(Hsv.fromArray([275, 75, 100])), + magenta: Hsv.toColor(Hsv.fromArray([300, 95, 100])), + hotpink: Hsv.toColor(Hsv.fromArray([335, 100, 100])), + pink: Hsv.toColor(Hsv.fromArray([350, 55, 100])), + peach: Hsv.toColor(Hsv.fromArray([25, 75, 100])), + lilac: Hsv.toColor(Hsv.fromArray([275, 55, 100])), + pinktint: Hsv.toColor(Hsv.fromArray([340, 30, 100])), + peachtint: Hsv.toColor(Hsv.fromArray([25, 50, 100])), + yellowtint: Hsv.toColor(Hsv.fromArray([60, 50, 100])), + greentint: Hsv.toColor(Hsv.fromArray([135, 40, 100])), + bluetint: Hsv.toColor(Hsv.fromArray([220, 40, 100])), + lilactint: Hsv.toColor(Hsv.fromArray([275, 35, 100])), + white: Hsv.toColor(Hsv.fromArray([0, 0, 100])), + gray: Hsv.toColor(Hsv.fromArray([0, 0, 50])), + brown: Hsv.toColor(Hsv.fromArray([20, 45, 75])), + deadwhite: Hsv.toColor(Hsv.fromArray([0, 0, 100])), + deadblack: Hsv.toColor(Hsv.fromArray([0, 0, 0])), + invisible: Hsv.toColor(Hsv.fromArray([0, 0, 0])) } const reWhitespaceComma = /[\s,]+/ @@ -77,9 +58,9 @@ const reTrimCurly = /^{+|}+$/g const reTrimQuotes = /^['"]+|['"]+$/g const reCollapseEqual = /\s*=\s*/g -function parseListDef (line: string, localColorDict: {[k: string]: number[]}) { +function parseListDef (line: string, localColorDict: {[k: string]: Color}) { let name - let defaultColor: number[] = localColorDict['white'] // Default color is white, but it can be overridden by the list definition + let defaultColor: Color = localColorDict['white'] // Default color is white, but it can be overridden by the list definition let radius let nobutton = false let master = [] @@ -125,7 +106,7 @@ function parseListDef (line: string, localColorDict: {[k: string]: number[]}) { } } -function parseListElm (line: string, localColorDict: {[k: string]: number[]}) { +function parseListElm (line: string, localColorDict: {[k: string]: Color}) { line = line.trim() const idx1 = line.indexOf('{') @@ -219,16 +200,20 @@ function parseGroup (line: string) { } function convertKinTriangleArrays (ribbonObject: RibbonObject) { // have to convert ribbons/triangle lists from stripdrawmode to normal drawmode - // index [ 0 1 2 3 4 5 6 7 8 91011 ] - // label [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ] + // index [ 0 1 2 3 4 5 6 7 8 91011 ] + // label/color [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ] // convertedindex [ 0 1 2 3 4 5 6 7 8 91011121314151617181920212223242526 ] // index [ 0 1 2 3 4 5 6 7 8 91011121314 ] [ 0 1 2 3 4 5 6 7 8 3 4 5 6 7 8 91011 6 7 8 91011121314 ] - // position/color [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ] + // position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ] let { labelArray, positionArray, colorArray, breakArray } = ribbonObject let convertedLabels = [] for (let i = 0; i < (labelArray.length - 2) * 3; ++i) { convertedLabels[i] = labelArray[i - Math.floor(i / 3) * 2] } + let convertedColors = [] + for (let i = 0; i < (colorArray.length - 2) * 3; ++i) { + convertedColors[i] = colorArray[i - Math.floor(i / 3) * 2] + } let convertedBreaks = [] for (let i = 0; i < (breakArray.length - 2) * 3; ++i) { convertedBreaks[i] = breakArray[i - Math.floor(i / 3) * 2] @@ -237,10 +222,6 @@ function convertKinTriangleArrays (ribbonObject: RibbonObject) { for (let i = 0; i < (positionArray.length / 3 - 2) * 9; ++i) { convertedPositions[i] = positionArray[i - Math.floor(i / 9) * 6] } - let convertedColors = [] - for (let i = 0; i < (colorArray.length / 3 - 2) * 9; ++i) { - convertedColors[i] = colorArray[i - Math.floor(i / 9) * 6] - } let vector3Positions = [] for (let i = 0; i < (convertedPositions.length) / 3; ++i) { vector3Positions.push([convertedPositions[i * 3], convertedPositions[i * 3] + 1, convertedPositions[i * 3] + 2]) @@ -268,8 +249,8 @@ function convertKinTriangleArrays (ribbonObject: RibbonObject) { function removePointBreaksTriangleArrays (convertedRibbonObject: RibbonObject) { // after converting ribbon/triangle arrys to drawmode, removed point break triangles - // label [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ] - // position/color [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ] + // label/color [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ] + // position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ] let { labelArray, positionArray, colorArray, breakArray } = convertedRibbonObject let editedLabels = [] let editedPositions = [] @@ -294,15 +275,9 @@ function removePointBreaksTriangleArrays (convertedRibbonObject: RibbonObject) { editedPositions.push(positionArray[positionPointer+6]) editedPositions.push(positionArray[positionPointer+7]) editedPositions.push(positionArray[positionPointer+8]) - editedColors.push(colorArray[positionPointer]) - editedColors.push(colorArray[positionPointer+1]) - editedColors.push(colorArray[positionPointer+2]) - editedColors.push(colorArray[positionPointer+3]) - editedColors.push(colorArray[positionPointer+4]) - editedColors.push(colorArray[positionPointer+5]) - editedColors.push(colorArray[positionPointer+6]) - editedColors.push(colorArray[positionPointer+7]) - editedColors.push(colorArray[positionPointer+8]) + editedColors.push(colorArray[breakPointer]) + editedColors.push(colorArray[breakPointer+1]) + editedColors.push(colorArray[breakPointer+2]) } else { //console.log('X triangle break found') //console.log('skipping: '+positionArray[positionPointer]+','+positionArray[positionPointer+1]+','+positionArray[positionPointer+2]+',' @@ -363,7 +338,7 @@ class KinParser { this.kinemage = kinemage // Keep a local copy of the ColorDict that we can update with new colors defined in the file. - let localColorDict: { [k: string]: number[] } = Object.assign({}, ColorDict) + let localColorDict: { [k: string]: Color } = Object.assign({}, ColorDict) let currentGroup: string = '' let currentGroupMasters: string[] @@ -372,28 +347,28 @@ class KinParser { let isDotList = false let prevDotLabel = '' - let dotDefaultColor: number[] - let dotLabel: string[], dotPosition: number[], dotColor: number[] + let dotDefaultColor: Color + let dotLabel: string[], dotPosition: number[], dotColor: Color[] let isVectorList = false let prevVecLabel = '' let prevVecPosition: number[]|null = null - let prevVecColor: number[]|null = null - let vecDefaultColor: number[], vecDefaultWidth: number - let vecLabel1: string[], vecLabel2: string[], vecPosition1: number[], vecPosition2: number[], vecColor1: number[], vecColor2: number[] + let prevVecColor: Color|null = null + let vecDefaultColor: Color, vecDefaultWidth: number + let vecLabel1: string[], vecLabel2: string[], vecPosition1: number[], vecPosition2: number[], vecColor1: Color[], vecColor2: Color[] let vecWidth: number[] let isBallList = false let prevBallLabel = '' - let ballRadius: number[], ballDefaultColor: number[], ballDefaultRadius: number - let ballLabel: string[], ballPosition: number[], ballColor: number[] + let ballRadius: number[], ballDefaultColor: Color, ballDefaultRadius: number + let ballLabel: string[], ballPosition: number[], ballColor: Color[] let isRibbonList = false let ribbonIsTriangles = false let prevRibbonPointLabel = '' - let ribbonListDefaultColor: number[] = localColorDict['white'] - let ribbonPointLabelArray: string[], ribbonPointPositionArray: number[], ribbonPointBreakArray: boolean[], ribbonPointColorArray: number[] + let ribbonListDefaultColor: Color = localColorDict['white'] + let ribbonPointLabelArray: string[], ribbonPointPositionArray: number[], ribbonPointBreakArray: boolean[], ribbonPointColorArray: Color[] let isText = false let isCaption = false @@ -435,7 +410,7 @@ class KinParser { dotLabel = [] dotPosition = [] dotColor = [] - dotDefaultColor = listColor as number[] + dotDefaultColor = listColor if (currentGroupMasters) { listMasters = listMasters.concat(currentGroupMasters) @@ -481,7 +456,7 @@ class KinParser { vecColor1 = [] vecColor2 = [] vecWidth = [] - vecDefaultColor = listColor as number[] + vecDefaultColor = listColor vecDefaultWidth = 2 if (listWidth) { vecDefaultWidth = listWidth @@ -529,7 +504,7 @@ class KinParser { ballRadius = [] ballPosition = [] ballColor = [] - ballDefaultColor = listColor as number[] + ballDefaultColor = listColor ballDefaultRadius = listRadius !== undefined ? listRadius : 1 if (currentGroupMasters) { @@ -570,7 +545,7 @@ class KinParser { ribbonPointPositionArray = [] ribbonPointBreakArray = [] ribbonPointColorArray = [] - ribbonListDefaultColor = listColor as number[] + ribbonListDefaultColor = listColor if (currentGroupMasters) { listMasters = listMasters.concat(currentGroupMasters) @@ -614,7 +589,7 @@ class KinParser { dotLabel.push(label) dotPosition.push(...position) - dotColor.push(...color) + dotColor.push(color) } else if (isVectorList) { // { n thr A 1 B13.79 1crnFH} P 17.047, 14.099, 3.625 { n thr A 1 B13.79 1crnFH} L 17.047, 14.099, 3.625 @@ -641,11 +616,11 @@ class KinParser { vecLabel1.push(prevVecLabel) vecPosition1.push(...prevVecPosition) - vecColor1.push(...prevVecColor as number[]) + vecColor1.push(prevVecColor ? prevVecColor : vecDefaultColor) vecLabel2.push(label) vecPosition2.push(...position) - vecColor2.push(...color as number[]) + vecColor2.push(color) vecWidth.push(width) } } @@ -676,7 +651,7 @@ class KinParser { ballLabel.push(label) ballRadius.push(radius) ballPosition.push(...position) - ballColor.push(...color) + ballColor.push(color) } else if (isRibbonList) { let { label, color, position, isTriangleBreak } = parseListElm(line, localColorDict) @@ -693,7 +668,8 @@ class KinParser { ribbonPointLabelArray.push(label) ribbonPointPositionArray.push(...position) ribbonPointBreakArray.push(isTriangleBreak) - ribbonPointColorArray.push(...color) + ribbonPointColorArray.push(color) + console.log('XXX Pushing ribbon color ' + color) } else if (isText) { kinemage.texts.push(line) } else if (isCaption) { diff --git a/src/extensions/kinemage/reader/schema.ts b/src/extensions/kinemage/reader/schema.ts index 149296a16..23039bc66 100644 --- a/src/extensions/kinemage/reader/schema.ts +++ b/src/extensions/kinemage/reader/schema.ts @@ -4,6 +4,8 @@ * @author ReliaSolve */ +import { Color } from '../../../mol-util/color'; + export interface Kinemage { readonly comments: ReadonlyArray kinemage?: number, @@ -41,31 +43,31 @@ export interface KinListBase { export interface DotList extends KinListBase { labelArray: string[], ///< Array of labels per element positionArray: number[], ///< Catenation of x, y, z for each element, 3x as many as elements - colorArray: number[] ///< Catenation of r, g, b for each element, 3x as many as elements + colorArray: Color[] ///< Color for each element, as many as elements } export interface BallList extends KinListBase { labelArray: string[], ///< Array of labels per element positionArray: number[], ///< Catenation of x, y, z for each element, 3x as many as elements - colorArray: number[], ///< Catenation of r, g, b for each element, 3x as many as elements + colorArray: Color[], ///< Color for each element, as many as elements radiusArray: number[] ///< A single radius per element } export interface RibbonObject extends KinListBase { labelArray: string[], ///< Array of labels per element positionArray: number[], ///< Catenation of x, y, z for each element, 9x as many as triangles (3 vertices per triangle) - colorArray: number[], ///< Catenation of r, g, b for each element, 9x as many as triangles (3 colors per triangle) + colorArray: Color[], ///< Color for each element, as many as elements breakArray: boolean[], ///< A single boolean per element indicating if there is a break there pairTriangleNormals: boolean ///< Whether to pair every other triangle normal for lighting (true for ribbons, false for triangles) } export interface VectorList extends KinListBase { - label1Array: string[], ///< Array of labels per element - label2Array: string[], ///< Array of labels per element + label1Array: string[], ///< Array of labels for the first half of each element + label2Array: string[], ///< Array of labels for the second half of each element position1Array: number[], ///< Catenation of x, y, z for each element, 3x as many as elements position2Array: number[], ///< Catenation of x, y, z for each element, 3x as many as elements - color1Array: number[], ///< Catenation of r, g, b for each element, 3x as many as elements - color2Array: number[], ///< Catenation of r, g, b for each element, 3x as many as elements + color1Array: Color[], ///< Color for first half of each element, as many as elements + color2Array: Color[], ///< Color for second half of each element, as many as elements width: number[] ///< A single width per element } diff --git a/src/mol-util/color/spaces/hsv.ts b/src/mol-util/color/spaces/hsv.ts new file mode 100644 index 000000000..de0867680 --- /dev/null +++ b/src/mol-util/color/spaces/hsv.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author ReliaSolve + * + * Adapted from kin-parser.ts file from the NGL project: + * @author Alexander Rose + * Adapted from hsl.ts in this same directory: + * @author David Sehnal + */ + +import type { Color } from '../color'; +import { Rgb } from './rgb'; + +export { Hsv }; + +/** Hsv tuple: [h, s, v] + * - h in [0,360] degrees + * - s in [0,100] percent + * - v in [0,100] percent + */ +interface Hsv extends Array { [d: number]: number, '@type': 'hsv', length: 3 } + +function Hsv() { + return Hsv.zero(); +} + +namespace Hsv { + export function zero(): Hsv { + const out = [0.0, 0.0, 0.0] + out[0] = 0 + return out as Hsv; + } + + /** Copy values from an array-like 3-tuple into `out`. */ + export function fromArray(arr: ArrayLike): Hsv { + const out = Hsv.zero(); + out[0] = arr[0] ?? 0 + out[1] = arr[1] ?? 0 + out[2] = arr[2] ?? 0 + return out + } + + const _rgb = Rgb(); + export function toColor(hsv: Hsv): Color { + toRgb(_rgb, hsv); + return Rgb.toColor(_rgb) + } + + export function toRgb(out: Rgb, hsv: Hsv) { + let [h, s, v] = hsv; + h /= 360 + s /= 100 + v /= 100 + let r = 0, g = 0, b = 0 + const i = Math.floor(h * 6) + const f = h * 6 - i + const p = v * (1 - s) + const q = v * (1 - f * s) + const t = v * (1 - (1 - f) * s) + switch (i % 6) { + case 0: r = v; g = t; b = p; break + case 1: r = q; g = v; b = p; break + case 2: r = p; g = v; b = t; break + case 3: r = p; g = q; b = v; break + case 4: r = t; g = p; b = v; break + case 5: r = v; g = p; b = q; break + } + out[0] = r + out[1] = g + out[2] = b + return out + } +}