File size: 8,142 Bytes
1a343cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { hsvToRgb, rgbToHsv } from './color';
import type { Alleles, ColorAlleles, ColorPattern, Hsv, Rgb, Stabilities } from './types';

const DOMINANT_HUE_ZONES: Array<[number, number]> = [
  [0, 20],
  [100, 140],
  [220, 260],
];
const THC_SOFT_CAP = 50;
const THC_SOFT_THRESHOLD = 31;

export type BloomScoreBreakdown = {
  score: number;
  colorCount: number;
  hueSpread: number;
  averageSaturation: number;
  averageValue: number;
  muddyStops: number;
  pattern: ColorPattern;
};

export type ColorExpression = {
  rgb: Rgb;
  hsv: Hsv;
  palette: Rgb[];
  hsvPalette: Hsv[];
  pattern: ColorPattern;
  contrast: number;
};

export type Phenotype = {
  thc: number;
  cbd: number;
  yield: number;
  growTime: number;
  budColorRgb: Rgb;
  leafColorRgb: Rgb;
  budColorHsv: Hsv;
  leafColorHsv: Hsv;
  budPaletteRgb: Rgb[];
  leafPaletteRgb: Rgb[];
  budPattern: ColorPattern;
  leafPattern: ColorPattern;
};

export function isPrimaryHue(hue: number): boolean {
  return DOMINANT_HUE_ZONES.some(([low, high]) => hue >= low && hue <= high);
}

function clamp01(value: number): number {
  return Math.max(0, Math.min(1, value));
}

function roundOne(value: number): number {
  return Math.round(value * 10) / 10;
}

function wrapHue(hue: number): number {
  return ((hue % 360) + 360) % 360;
}

function dominanceExpression(values: [number, number], stability = 0.5, lowDominates = false): number {
  const boundedStability = clamp01(stability);
  const dominant = lowDominates ? Math.min(...values) : Math.max(...values);
  const recessive = lowDominates ? Math.max(...values) : Math.min(...values);
  const dominanceWeight = 0.58 + boundedStability * 0.34;
  return dominant * dominanceWeight + recessive * (1 - dominanceWeight);
}

function hueDistance(left: number, right: number): number {
  const distance = Math.abs((((left - right) % 360) + 540) % 360 - 180);
  return roundOne(distance);
}

function mixHue(left: number, right: number, weightRight: number): number {
  const delta = (((right - left) % 360) + 540) % 360 - 180;
  return wrapHue(left + delta * weightRight);
}

function uniqueHsvPalette(colors: Hsv[]): Hsv[] {
  const seen = new Set<string>();
  const unique: Hsv[] = [];
  for (const color of colors) {
    const key = `${Math.round(color[0] / 8)}_${Math.round(color[1] * 10)}_${Math.round(color[2] * 10)}`;
    if (!seen.has(key)) {
      seen.add(key);
      unique.push(color);
    }
  }
  return unique.slice(0, 6);
}

function mean(values: number[]): number {
  return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0;
}

function hueSpread(hues: number[]): number {
  if (hues.length <= 1) return 0;
  let maxDistance = 0;
  for (let left = 0; left < hues.length; left += 1) {
    for (let right = left + 1; right < hues.length; right += 1) {
      const leftHue = hues[left];
      const rightHue = hues[right];
      if (leftHue === undefined || rightHue === undefined) continue;
      maxDistance = Math.max(maxDistance, hueDistance(leftHue, rightHue));
    }
  }
  return maxDistance;
}

export function bloomScoreFromPalette(palette: Rgb[], pattern: ColorPattern = 'solid'): BloomScoreBreakdown {
  const hsvStops = uniqueHsvPalette(palette.map((color) => rgbToHsv(color)));
  if (!hsvStops.length) {
    return { score: 0, colorCount: 0, hueSpread: 0, averageSaturation: 0, averageValue: 0, muddyStops: 0, pattern };
  }
  const colorCount = hsvStops.length;
  const spread = hueSpread(hsvStops.map((hsv) => hsv[0]));
  const averageSaturation = mean(hsvStops.map((hsv) => hsv[1]));
  const averageValue = mean(hsvStops.map((hsv) => hsv[2]));
  const muddyStops = hsvStops.filter((hsv) => hsv[1] < 0.34 || hsv[2] < 0.28).length;
  const patternBonus: Record<ColorPattern, number> = { solid: 0, speckled: 4, split: 8, rimmed: 10, mosaic: 16, polycolor: 22 };
  const zoneScore = Math.min(1, Math.max(0, (colorCount - 1) / 5)) * 24;
  const spreadScore = (spread / 180) * 22;
  const colorQuality = averageSaturation * 34 + averageValue * 10;
  const muddyPenalty = (muddyStops / colorCount) * 18;
  const score = Math.max(0, Math.min(100, colorQuality + zoneScore + spreadScore + patternBonus[pattern] - muddyPenalty));
  return {
    score: Math.round(score * 10) / 10,
    colorCount,
    hueSpread: Math.round(spread * 10) / 10,
    averageSaturation: Math.round(averageSaturation * 1000) / 1000,
    averageValue: Math.round(averageValue * 1000) / 1000,
    muddyStops,
    pattern,
  };
}

export function expressThcPotential(values: [number, number], stability = 0.5): number {
  const raw = dominanceExpression(values, stability);
  if (raw <= THC_SOFT_THRESHOLD) return raw;
  const pressure = 0.9 + clamp01(stability) * 0.35;
  const excess = raw - THC_SOFT_THRESHOLD;
  return THC_SOFT_THRESHOLD + (THC_SOFT_CAP - THC_SOFT_THRESHOLD) * (1 - Math.exp(-(excess * pressure) / 12));
}

export function expressColorAlleles(hsvAlleles: ColorAlleles, stability = 1): ColorExpression {
  const boundedStability = clamp01(stability);
  const volatility = 1 - boundedStability;
  const hsv0: Hsv = [wrapHue(hsvAlleles.Hue[0]), clamp01(hsvAlleles.Saturation[0]), clamp01(hsvAlleles.Value[0])];
  const hsv1: Hsv = [wrapHue(hsvAlleles.Hue[1]), clamp01(hsvAlleles.Saturation[1]), clamp01(hsvAlleles.Value[1])];
  const strength = (hsv: Hsv) => (hsv[1] * 0.68 + hsv[2] * 0.32) * (1 + boundedStability);
  const strength0 = strength(hsv0) + (isPrimaryHue(hsv0[0]) ? 0.08 : 0);
  const strength1 = strength(hsv1) + (isPrimaryHue(hsv1[0]) ? 0.08 : 0);
  const primary = strength0 >= strength1 ? hsv0 : hsv1;
  const secondary = primary === hsv0 ? hsv1 : hsv0;
  const contrast = hueDistance(hsv0[0], hsv1[0]);
  const balance = Math.min(strength0, strength1) / Math.max(0.001, Math.max(strength0, strength1));
  const reveal = contrast / 180 * 0.42 + balance * 0.32 + volatility * 0.42;
  const visibleColors = Math.max(1, Math.min(6, Math.ceil(reveal * 6 - boundedStability * 1.25)));
  const colors: Hsv[] = [primary];

  if (visibleColors >= 2) colors.push(secondary);
  if (visibleColors >= 3) colors.push([mixHue(primary[0], secondary[0], 0.33), Math.max(primary[1], secondary[1]) * 0.95, Math.min(1, Math.max(primary[2], secondary[2]) * 1.04)]);
  if (visibleColors >= 4) colors.push([mixHue(primary[0], secondary[0], 0.66), Math.min(1, secondary[1] + volatility * 0.14), Math.min(1, secondary[2] + volatility * 0.1)]);
  if (visibleColors >= 5) colors.push([wrapHue(primary[0] + 145 + volatility * 35), Math.min(1, primary[1] * 0.86 + volatility * 0.18), Math.min(1, primary[2] * 0.92 + 0.08)]);
  if (visibleColors >= 6) colors.push([wrapHue(secondary[0] - 120 - volatility * 24), Math.min(1, secondary[1] * 0.9 + 0.08), Math.max(0.2, secondary[2] * 0.72)]);

  const hsvPalette = uniqueHsvPalette(colors);
  const pattern: ColorPattern = (() => {
    if (hsvPalette.length <= 1) return 'solid';
    if (hsvPalette.length >= 6) return 'polycolor';
    if (hsvPalette.length >= 4) return 'mosaic';
    if (contrast >= 128 && boundedStability >= 0.38) return 'rimmed';
    if (contrast >= 72) return 'split';
    return 'speckled';
  })();

  return {
    rgb: hsvToRgb(primary[0], primary[1], primary[2]),
    hsv: primary,
    palette: hsvPalette.map((hsv) => hsvToRgb(hsv[0], hsv[1], hsv[2])),
    hsvPalette,
    pattern,
    contrast,
  };
}

export function generatePhenotype(alleles: Alleles, stabilities: Stabilities): Phenotype {
  const budColor = expressColorAlleles(alleles.BudColor, stabilities.BudColor);
  const leafColor = expressColorAlleles(alleles.LeafColor, stabilities.LeafColor);

  return {
    thc: expressThcPotential(alleles.THC, stabilities.THC),
    cbd: dominanceExpression(alleles.CBD, stabilities.CBD),
    yield: dominanceExpression(alleles.Yield, stabilities.Yield),
    growTime: dominanceExpression(alleles.GrowTime, stabilities.GrowTime, true),
    budColorRgb: budColor.rgb,
    leafColorRgb: leafColor.rgb,
    budColorHsv: budColor.hsv,
    leafColorHsv: leafColor.hsv,
    budPaletteRgb: budColor.palette,
    leafPaletteRgb: leafColor.palette,
    budPattern: budColor.pattern,
    leafPattern: leafColor.pattern,
  };
}