Skip to content

Commit ad09251

Browse files
authored
Merge pull request #37 from peelar/revert-35-feat/improve-gradient-quality
Revert "Improve gradient quality with contrast enforcement and layout-aware geometry"
2 parents 2db4ac4 + a7b5040 commit ad09251

5 files changed

Lines changed: 13 additions & 197 deletions

File tree

components/layouts/shared/background-style.ts

Lines changed: 7 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,18 @@
1-
import { LayoutConfig, CustomGradient } from "@/domain/layout/types";
1+
import { LayoutConfig } from "@/domain/layout/types";
22
import { Asset } from "@/domain/asset/types";
3-
import { customGradientToCss, isAdvancedGradient, isLegacyGradient } from "@/domain/layout/gradients";
3+
import { customGradientToCss } from "@/domain/layout/gradients";
44
import { getGradientById } from "@/domain/layout/gradient-presets";
55
import { tokenToCssColor } from "@/components/layouts/shared/color-utils";
66

7-
type LayoutGeometry =
8-
| { type: "radial"; direction: string }
9-
| { type: "linear"; angle: number };
10-
11-
/**
12-
* Get layout-specific gradient geometry based on layout type and variant.
13-
* This allows the same gradient colors to render differently per layout.
14-
*
15-
* Note: We use linear gradients for all layouts because the 3-stop gradient
16-
* structure (color → dark → color) doesn't work well with radial gradients
17-
* (creates a glow/ring effect instead of smooth coverage).
18-
*/
19-
function getLayoutGeometry(layoutId: string, variant?: string): LayoutGeometry {
20-
// Spotlight: diagonal toward screenshot side
21-
if (layoutId.startsWith("hero-center")) {
22-
// Left variant: text on left, screenshot on right → gradient flows left-to-right
23-
// Right variant: text on right, screenshot on left → gradient flows right-to-left
24-
return {
25-
type: "linear",
26-
angle: variant === "right" ? 270 : 90,
27-
};
28-
}
29-
30-
// Peak: linear perpendicular to screenshot edge
31-
if (layoutId.startsWith("popup-gradient")) {
32-
const angles: Record<string, number> = {
33-
left: 90,
34-
right: 270,
35-
center: 180,
36-
};
37-
return { type: "linear", angle: angles[variant ?? "center"] ?? 180 };
38-
}
39-
40-
// Backdrop: vertical gradient for centered screenshot
41-
if (layoutId === "adaptive-stage") {
42-
return { type: "linear", angle: 180 };
43-
}
44-
45-
// Code snippet: diagonal linear
46-
if (layoutId.startsWith("code-snippet")) {
47-
return { type: "linear", angle: 135 };
48-
}
49-
50-
// Default: diagonal linear
51-
return { type: "linear", angle: 135 };
52-
}
53-
54-
/**
55-
* Extract gradient stops as a CSS string from a CustomGradient
56-
*/
57-
function getGradientStopsString(gradient: CustomGradient): string {
58-
if (isAdvancedGradient(gradient)) {
59-
return gradient.stops
60-
.map((stop) => {
61-
if (stop.position !== undefined) {
62-
const position = stop.position <= 1 ? `${stop.position * 100}%` : `${stop.position}%`;
63-
return `${stop.color} ${position}`;
64-
}
65-
return stop.color;
66-
})
67-
.join(", ");
68-
}
69-
70-
if (isLegacyGradient(gradient)) {
71-
return `${gradient.from}, ${gradient.to}`;
72-
}
73-
74-
return "#6366f1, #8b5cf6";
75-
}
76-
77-
/**
78-
* Convert gradient to CSS with layout-specific geometry.
79-
* Colors come from the stored gradient; geometry is determined by layout type.
80-
*/
81-
function gradientToCssWithLayout(
82-
gradient: CustomGradient,
83-
layoutId: string,
84-
variant?: string
85-
): string {
86-
const stops = getGradientStopsString(gradient);
87-
const geometry = getLayoutGeometry(layoutId, variant);
88-
89-
if (geometry.type === "radial") {
90-
return `radial-gradient(${geometry.direction}, ${stops})`;
91-
}
92-
return `linear-gradient(${geometry.angle}deg, ${stops})`;
93-
}
94-
957
export function getBackgroundStyle(config: LayoutConfig, assetMap: Map<string, Asset>): string {
968
if (config.background?.type === "gradient") {
97-
const gradient =
98-
config.background.customGradient ?? getGradientById(config.background.value)?.gradient;
9+
if (config.background.customGradient) {
10+
return customGradientToCss(config.background.customGradient);
11+
}
9912

13+
const gradient = getGradientById(config.background.value);
10014
if (gradient) {
101-
// Apply layout-specific geometry at render time
102-
return gradientToCssWithLayout(gradient, config.layoutId, config.variant);
15+
return customGradientToCss(gradient.gradient);
10316
}
10417
} else if (config.background?.type === "image") {
10518
const bgAsset = assetMap.get(config.background.value);

domain/gradient-generation/color-extraction.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,7 @@ function classifyClusters(
336336
const saturationScore = avgSaturation * saturationWeight;
337337
const balanceScore = 1 - Math.abs(avgLightness - 0.5);
338338
const accentScore = saturationScore * 0.7 + balanceScore * 0.3;
339-
// Stricter threshold to avoid picking muddy/neutral colors as accents
340-
const isAccent = accentScore > 0.5 && avgSaturation > 0.4;
339+
const isAccent = accentScore > 0.45 && avgSaturation > 0.35;
341340

342341
return {
343342
hex,

domain/layout/gradients/colors.ts

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -25,47 +25,6 @@ function oklchToHex(color: Oklch): string {
2525
}
2626
}
2727

28-
/**
29-
* Enforce minimum chromatic separation between two colors.
30-
* If colors are too similar in both hue and luminance, artificially
31-
* create contrast by pushing one color darker or lighter.
32-
*
33-
* This ensures gradients have visible direction rather than appearing muddy.
34-
*/
35-
export function enforceColorSeparation(
36-
colorA: string,
37-
colorB: string,
38-
minHueDelta = 30,
39-
minLightnessDelta = 0.25
40-
): { colorA: string; colorB: string } {
41-
const oklchA = hexToOklch(colorA);
42-
const oklchB = hexToOklch(colorB);
43-
44-
if (!oklchA || !oklchB) {
45-
return { colorA, colorB };
46-
}
47-
48-
// Calculate hue delta (handle wraparound at 360)
49-
const hueA = oklchA.h ?? 0;
50-
const hueB = oklchB.h ?? 0;
51-
const rawHueDelta = Math.abs(hueA - hueB);
52-
const hueDelta = Math.min(rawHueDelta, 360 - rawHueDelta);
53-
54-
const lightnessDelta = Math.abs(oklchA.l - oklchB.l);
55-
56-
// If both deltas are below threshold, invent contrast
57-
if (hueDelta < minHueDelta && lightnessDelta < minLightnessDelta) {
58-
// Push colorB darker if colorA is light, lighter if colorA is dark
59-
const shift = oklchA.l > 0.5 ? -0.35 : 0.35;
60-
return {
61-
colorA,
62-
colorB: enhanceColor(colorB, { lightnessShift: shift }),
63-
};
64-
}
65-
66-
return { colorA, colorB };
67-
}
68-
6928
/**
7029
* Enhanced color palette with categorized colors for gradient generation
7130
*/
@@ -167,35 +126,6 @@ function isDarkPalette(palette: ColorPalette): boolean {
167126
return avgLightness < 0.4; // Dark if average lightness < 0.4
168127
}
169128

170-
/**
171-
* Mix a color with a bias anchor color in OKLCH space.
172-
* Used to inject warm/cool tones into neutral palettes.
173-
*/
174-
function mixWithBias(hex: string, biasHex: string, ratio: number): string {
175-
const oklchBase = hexToOklch(hex);
176-
const oklchBias = hexToOklch(biasHex);
177-
178-
if (!oklchBase || !oklchBias) return hex;
179-
180-
// Mix lightness and chroma, but shift hue toward bias
181-
const l = oklchBase.l * (1 - ratio * 0.5) + oklchBias.l * (ratio * 0.5);
182-
const c = Math.max(oklchBase.c ?? 0, (oklchBias.c ?? 0) * ratio * 0.8);
183-
184-
// Blend hue toward bias color
185-
const baseH = oklchBase.h ?? 0;
186-
const biasH = oklchBias.h ?? 0;
187-
const h = baseH + (biasH - baseH) * ratio;
188-
189-
const mixed: Oklch = {
190-
mode: "oklch",
191-
l: Math.max(0.1, Math.min(0.9, l)),
192-
c: Math.min(0.35, c),
193-
h: ((h % 360) + 360) % 360,
194-
};
195-
196-
return oklchToHex(mixed);
197-
}
198-
199129
/**
200130
* Enhance and categorize color palette for gradient generation
201131
* Transforms raw screenshot colors into a structured palette for creating beautiful gradients
@@ -239,16 +169,6 @@ export function enhanceColorPalette(palette: ColorPalette): EnhancedColorPalette
239169
hero = enhanceColor(hero, { saturationBoost: 0.5 });
240170
}
241171

242-
// If truly neutral (very low saturation), inject warm/cool bias
243-
// This ensures neutral screenshots don't produce neutral (muddy) gradients
244-
if (isNeutral && saturation < 0.08) {
245-
const warmAnchor = "#f97316"; // orange
246-
const coolAnchor = "#6366f1"; // indigo
247-
// Pick based on average lightness - dark palettes get warm, light get cool
248-
const biasColor = isDark ? warmAnchor : coolAnchor;
249-
hero = mixWithBias(hero ?? base, biasColor, 0.4);
250-
}
251-
252172
if (isDark) {
253173
// Lighten the base for dark palettes to improve contrast
254174
base = enhanceColor(base, { lightnessShift: 0.3 });

domain/layout/gradients/generator.ts

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { AspectCategory } from "../aspect";
22
import { AdvancedGradient, GradientStop, GradientType } from "./types";
33
import { ColorPalette } from "@/domain/asset/types";
4-
import {
5-
enhanceColorPalette,
6-
EnhancedColorPalette,
7-
enhanceColor,
8-
enforceColorSeparation,
9-
} from "./colors";
4+
import { enhanceColorPalette, EnhancedColorPalette, enhanceColor } from "./colors";
105

116
type GradientVariation = {
127
colorPair?: [string, string];
@@ -128,20 +123,14 @@ const BACKGROUND_GRADIENT_COLORS = [
128123
];
129124

130125
/**
131-
* Generate gradient stops with balanced color distribution.
132-
* Uses two screenshot colors + an ambient background color.
133-
* Enforces chromatic separation to prevent muddy gradients.
126+
* Generate three color stops: two most prominent screenshot colors + an ambient background color
134127
*/
135128
function generateGradientStops(
136129
palette: EnhancedColorPalette,
137130
strategy: "hero-base" | "multi-color" | "complementary" | "analogous" | "triadic" = "multi-color",
138131
variation?: GradientVariation,
139132
): GradientStop[] {
140-
const [rawPrimary, rawSecondary] = variation?.colorPair ?? getProminentScreenshotColors(palette);
141-
142-
// Enforce chromatic separation to prevent muddy gradients
143-
const { colorA: primary, colorB: secondary } = enforceColorSeparation(rawPrimary, rawSecondary);
144-
133+
const [primary, secondary] = variation?.colorPair ?? getProminentScreenshotColors(palette);
145134
const seed = variation?.backgroundSeed ?? `${primary}-${secondary}-${strategy}`;
146135
const background = getBackgroundAccentColor(
147136
seed,
@@ -164,14 +153,9 @@ function generateGradientStops(
164153
strategy,
165154
);
166155

167-
// Use 4 stops to create balanced color distribution:
168-
// - 0-35%: transition from start to background
169-
// - 35-65%: hold background color (gives it ~30% visual presence)
170-
// - 65-100%: transition from background to end
171156
return [
172157
{ color: start, position: 0 },
173-
{ color: background, position: 35 },
174-
{ color: background, position: 65 },
158+
{ color: background, position: 50 },
175159
{ color: end, position: 100 },
176160
];
177161
}

domain/layout/gradients/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type GradientColorSpace = "oklch" | "srgb" | "lab";
3232
*/
3333
export type AdvancedGradient = {
3434
type: GradientType;
35-
stops: GradientStop[]; // Color stops with positions
35+
stops: GradientStop[]; // Always three stops (start, background, end)
3636
direction?: string; // e.g., "to right", "45deg", "circle at center"
3737
colorSpace?: GradientColorSpace; // defaults to "oklch" for perceptual uniformity
3838
angle?: number; // for linear gradients in degrees (0-360)

0 commit comments

Comments
 (0)