@omerdlw/base-framework 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,18 @@
1
1
  import type { AmbientPalette, OklchColor } from "./types";
2
2
  export declare const COLOR_EXTRACT_CONFIG: Readonly<{
3
3
  readonly blackChromaFactor: 0.35;
4
- readonly primaryLightness: 0.74;
5
- readonly maxBlackChroma: 0.055;
6
4
  readonly blackLightness: 0.15;
7
5
  readonly cacheLimit: 100;
6
+ readonly chromaGain: 9.4;
7
+ readonly hueBins: 36;
8
+ readonly maxBlackChroma: 0.055;
8
9
  readonly maxChroma: 0.28;
9
- readonly minChroma: 0.03;
10
- readonly sampleSize: 32;
10
+ readonly maxPrimaryChroma: 0.24;
11
+ readonly minPixelChroma: 0.03;
12
+ readonly minPresence: 0.04;
13
+ readonly neutralChroma: 0.012;
14
+ readonly primaryLightness: 0.74;
15
+ readonly sampleSize: 64;
11
16
  }>;
12
17
  export declare function srgbToLinear(c: number): number;
13
18
  export declare function rgbToOklch(r: number, g: number, b: number): OklchColor;
@@ -1 +1 @@
1
- {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../../src/modules/ambient/color.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;EAStB,CAAC;AAEZ,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAyBtE;AAED,wBAAgB,aAAa,CAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,KAAK,GAAE,MAAM,GAAG,MAAU,GACzB,MAAM,CASR;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,CAuBjE"}
1
+ {"version":3,"file":"color.d.ts","sourceRoot":"","sources":["../../../src/modules/ambient/color.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;EActB,CAAC;AAEZ,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAyBtE;AAED,wBAAgB,aAAa,CAC3B,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,CAAC,EAAE,MAAM,GAAG,MAAM,EAClB,KAAK,GAAE,MAAM,GAAG,MAAU,GACzB,MAAM,CASR;AAMD,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,CAgCjE"}
@@ -1,4 +1,12 @@
1
1
  import type { AmbientExtractOptions, AmbientImageSource, AmbientPalette, OklchColor } from "./types";
2
+ /**
3
+ * Picks the most representative accent of an image.
4
+ *
5
+ * Pixels vote for their hue with a weight that grows with chroma (vivid pixels
6
+ * beat muddy ones) and fades toward very dark / very light pixels. Images
7
+ * without meaningful colour resolve to a neutral (`c: 0`) instead of an
8
+ * invented hue.
9
+ */
2
10
  export declare function sampleImageData(imageData: ImageData): OklchColor;
3
11
  export declare function createDefaultPalette(fallback?: AmbientPalette | null): AmbientPalette;
4
12
  export declare function extractPaletteFromImage(imageSource: AmbientImageSource, options?: AmbientExtractOptions): Promise<AmbientPalette>;
@@ -1 +1 @@
1
- {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/modules/ambient/extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,UAAU,EACX,MAAM,SAAS,CAAC;AAEjB,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAsFhE;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,GAC/B,cAAc,CAKhB;AAsED,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,kBAAkB,EAC/B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CAqBzB"}
1
+ {"version":3,"file":"extractor.d.ts","sourceRoot":"","sources":["../../../src/modules/ambient/extractor.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,UAAU,EACX,MAAM,SAAS,CAAC;AAUjB;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAsFhE;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,CAAC,EAAE,cAAc,GAAG,IAAI,GAC/B,cAAc,CAKhB;AAsED,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,kBAAkB,EAC/B,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CAqBzB"}
@@ -175,13 +175,18 @@ var ambientTheme = defineThemeSpec("ambient");
175
175
  // src/modules/ambient/color.ts
176
176
  var COLOR_EXTRACT_CONFIG = Object.freeze({
177
177
  blackChromaFactor: 0.35,
178
- primaryLightness: 0.74,
179
- maxBlackChroma: 0.055,
180
178
  blackLightness: 0.15,
181
179
  cacheLimit: 100,
180
+ chromaGain: 9.4,
181
+ hueBins: 36,
182
+ maxBlackChroma: 0.055,
182
183
  maxChroma: 0.28,
183
- minChroma: 0.03,
184
- sampleSize: 32
184
+ maxPrimaryChroma: 0.24,
185
+ minPixelChroma: 0.03,
186
+ minPresence: 0.04,
187
+ neutralChroma: 0.012,
188
+ primaryLightness: 0.74,
189
+ sampleSize: 64
185
190
  });
186
191
  function srgbToLinear(c) {
187
192
  const v = c / 255;
@@ -218,100 +223,104 @@ function oklchToString(l, c, h, alpha = 1) {
218
223
  }
219
224
  return `oklch(${safeL} ${safeC} ${safeH})`;
220
225
  }
226
+ function clamp(value, min, max) {
227
+ return Math.max(min, Math.min(max, value));
228
+ }
221
229
  function derivePalette(sampled) {
222
230
  const {
223
231
  blackChromaFactor,
224
232
  blackLightness,
233
+ chromaGain,
225
234
  maxBlackChroma,
226
235
  maxChroma,
227
- minChroma,
236
+ maxPrimaryChroma,
237
+ neutralChroma,
228
238
  primaryLightness
229
239
  } = COLOR_EXTRACT_CONFIG;
230
- const chroma = Math.max(minChroma, Math.min(maxChroma, sampled.c));
231
- const blackChroma = Math.min(
232
- maxBlackChroma,
233
- Math.max(0.025, chroma * blackChromaFactor)
234
- );
235
- const primaryChroma = Math.max(
236
- 0.15,
237
- Math.min(0.24, Math.max(chroma * 1.15, 0.18))
240
+ const chroma = sampled.c < neutralChroma ? 0 : clamp(sampled.c, 0, maxChroma);
241
+ const hue = chroma === 0 ? 0 : sampled.h;
242
+ const primaryChroma = maxPrimaryChroma * (1 - Math.exp(-chroma * chromaGain));
243
+ const primaryL = clamp(
244
+ primaryLightness + (sampled.l - 0.6) * 0.5,
245
+ 0.6,
246
+ 0.86
238
247
  );
248
+ const blackChroma = Math.min(maxBlackChroma, chroma * blackChromaFactor);
249
+ const blackL = clamp(blackLightness + (sampled.l - 0.6) * 0.06, 0.12, 0.18);
239
250
  return {
240
- black: oklchToString(blackLightness, blackChroma, sampled.h),
241
- primary: oklchToString(primaryLightness, primaryChroma, sampled.h)
251
+ black: oklchToString(blackL, blackChroma, hue),
252
+ primary: oklchToString(primaryL, primaryChroma, hue)
242
253
  };
243
254
  }
244
255
 
245
256
  // src/modules/ambient/extractor.ts
246
257
  function sampleImageData(imageData) {
258
+ const { hueBins, minPixelChroma, minPresence } = COLOR_EXTRACT_CONFIG;
247
259
  const data = imageData.data;
248
- const len = data.length;
249
- const numBins = 24;
250
- const binAngle = 360 / numBins;
251
- const bins = Array.from({ length: numBins }, () => ({
252
- count: 0,
260
+ const binAngle = 360 / hueBins;
261
+ const bins = Array.from({ length: hueBins }, () => ({
262
+ cos: 0,
263
+ sin: 0,
253
264
  sumC: 0,
254
- sumCos: 0,
255
265
  sumL: 0,
256
- sumSin: 0
266
+ weight: 0
257
267
  }));
258
- let neutralCount = 0;
259
- let neutralSumL = 0;
268
+ let opaqueCount = 0;
269
+ let opaqueSumL = 0;
260
270
  let chromaticCount = 0;
261
- for (let i = 0; i < len; i += 4) {
262
- const alpha = data[i + 3];
263
- if (alpha < 128) continue;
264
- const r = data[i];
265
- const g = data[i + 1];
266
- const b = data[i + 2];
267
- const oklch = rgbToOklch(r, g, b);
268
- if (oklch.l < 0.04 || oklch.l > 0.96 || oklch.c < 0.02) {
269
- neutralCount++;
270
- neutralSumL += oklch.l;
271
+ let midNeutralCount = 0;
272
+ for (let i = 0; i < data.length; i += 4) {
273
+ if (data[i + 3] < 128) continue;
274
+ const { c, h, l } = rgbToOklch(data[i], data[i + 1], data[i + 2]);
275
+ opaqueCount++;
276
+ opaqueSumL += l;
277
+ if (l < 0.06 || l > 0.96) continue;
278
+ if (c < minPixelChroma) {
279
+ midNeutralCount++;
271
280
  continue;
272
281
  }
273
- const binIndex = Math.floor(oklch.h / binAngle) % numBins;
274
- const rad = oklch.h * (Math.PI / 180);
275
- const bin = bins[binIndex];
276
- bin.count++;
277
- bin.sumL += oklch.l;
278
- bin.sumC += oklch.c;
279
- bin.sumCos += Math.cos(rad);
280
- bin.sumSin += Math.sin(rad);
282
+ const usable = 1 - 0.7 * Math.min(1, Math.abs(l - 0.62) / 0.62);
283
+ const weight2 = c * c * usable;
284
+ const rad = h * (Math.PI / 180);
285
+ const bin = bins[Math.floor(h / binAngle) % hueBins];
286
+ bin.weight += weight2;
287
+ bin.sumC += c * weight2;
288
+ bin.sumL += l * weight2;
289
+ bin.cos += Math.cos(rad) * weight2;
290
+ bin.sin += Math.sin(rad) * weight2;
281
291
  chromaticCount++;
282
292
  }
283
- if (chromaticCount === 0) {
284
- const avgL2 = neutralCount > 0 ? neutralSumL / neutralCount : 0.55;
285
- return { c: 0.03, h: 252, l: avgL2 };
286
- }
293
+ const averageL = opaqueCount > 0 ? opaqueSumL / opaqueCount : 0.5;
294
+ const neutral = { c: 0, h: 0, l: Number(averageL.toFixed(3)) };
295
+ const presence = chromaticCount / Math.max(1, chromaticCount + midNeutralCount);
296
+ if (chromaticCount === 0 || presence < minPresence) return neutral;
287
297
  let bestIndex = 0;
288
- let maxWindowCount = -1;
289
- for (let i = 0; i < numBins; i++) {
290
- const prev2 = bins[(i - 1 + numBins) % numBins];
291
- const curr2 = bins[i];
292
- const next2 = bins[(i + 1) % numBins];
293
- const windowCount = prev2.count * 0.5 + curr2.count + next2.count * 0.5;
294
- if (windowCount > maxWindowCount) {
295
- maxWindowCount = windowCount;
298
+ let bestScore = -1;
299
+ for (let i = 0; i < hueBins; i++) {
300
+ const prev = bins[(i - 1 + hueBins) % hueBins];
301
+ const next = bins[(i + 1) % hueBins];
302
+ const score = prev.weight * 0.5 + bins[i].weight + next.weight * 0.5;
303
+ if (score > bestScore) {
304
+ bestScore = score;
296
305
  bestIndex = i;
297
306
  }
298
307
  }
299
- const prev = bins[(bestIndex - 1 + numBins) % numBins];
300
- const curr = bins[bestIndex];
301
- const next = bins[(bestIndex + 1) % numBins];
302
- const totalCount = prev.count + curr.count + next.count;
303
- if (totalCount === 0) {
304
- return { c: 0.19, h: 252, l: 0.55 };
305
- }
306
- const avgL = (prev.sumL + curr.sumL + next.sumL) / totalCount;
307
- const avgC = (prev.sumC + curr.sumC + next.sumC) / totalCount;
308
- const avgSin = prev.sumSin + curr.sumSin + next.sumSin;
309
- const avgCos = prev.sumCos + curr.sumCos + next.sumCos;
310
- const avgH = (Math.atan2(avgSin, avgCos) * (180 / Math.PI) + 360) % 360;
308
+ const window2 = [
309
+ bins[(bestIndex - 1 + hueBins) % hueBins],
310
+ bins[bestIndex],
311
+ bins[(bestIndex + 1) % hueBins]
312
+ ];
313
+ const weight = window2.reduce((sum, bin) => sum + bin.weight, 0);
314
+ if (weight === 0) return neutral;
315
+ const sumC = window2.reduce((sum, bin) => sum + bin.sumC, 0);
316
+ const sumL = window2.reduce((sum, bin) => sum + bin.sumL, 0);
317
+ const sin = window2.reduce((sum, bin) => sum + bin.sin, 0);
318
+ const cos = window2.reduce((sum, bin) => sum + bin.cos, 0);
319
+ const dilution = 0.45 + 0.55 * Math.min(1, presence / 0.3);
311
320
  return {
312
- c: Number(avgC.toFixed(3)),
313
- h: Number(avgH.toFixed(1)),
314
- l: Number(avgL.toFixed(3))
321
+ c: Number((sumC / weight * dilution).toFixed(3)),
322
+ h: Number(((Math.atan2(sin, cos) * (180 / Math.PI) + 360) % 360).toFixed(1)),
323
+ l: Number((sumL / weight).toFixed(3))
315
324
  };
316
325
  }
317
326
  function createDefaultPalette(fallback) {