COMPO · 1.10 · POINT COLOR / TONE

Цвет
и тон
точек

Исправленный локальный модуль. Он измеряет отношение точка ↔ реальный фон под ней: относительную яркость, тональный контраст, цветовую дистанцию и устойчивость различимости на неоднородном фоне. Он не назначает глобальную цветовую иерархию и не вычисляет visualWeight.

Контракт модуля

00 / strict API

CONSUMES

Point.visualRole ← Compo 1.00 Point.functionalRole ← Compo 1.00 Point.subroles[] ← Compo 1.00 Point.radiusShortN ← Compo 1.01 Point.position ← placement result/candidate Scene.backgroundSampler ← engine Scene.paletteConstraints ← style/system StylePreset.colorPolicy ← STYLE_PRESET Global.colorHierarchy ← DEFERRED_INPUT

PROVIDES

Point.localContrast // canonical [0..1] Point.tonalContrastRatio // diagnostic [1..21] Point.tonalContrastN // [0..1] Point.oklabDistance // >= 0 Point.backgroundComplexity // [0..1] Point.colorSeparationScore // [0..1] Point.colorCandidates[] Point.colorDiagnostics

OWNS METRICS

point.local_contrast point.local_tonal_contrast point.local_oklab_distance point.background_complexity point.color_separation_score point.color_candidate_fit

Compo 1.01 только читает point.local_contrast и сам преобразует его в weight-modifier.

Запрет двойного ownership: этот модуль НЕ предоставляет colorAttentionModifier и НЕ пишет Point.visualWeightContextual. Преобразование localContrast → contrastModifier принадлежит владельцу visualWeight — Compo 1.01.

Две оси ролей — не смешивать

01 / role policy

VisualRole задаёт желаемую силу

visualRole = focus | support | background STYLE/HEURISTIC target bands: focus → high separation support → medium separation background → low separation

Это не функциональная роль и не гарантия. Глобальная hierarchy-система может изменить band.

FunctionalRole задаёт нижний floor

functionalRole = free | anchor | node | rhythm_member | marker example: anchor/marker may require minimum local visibility regardless of visualRole

Например anchor + background остаётся фоновым, но не должен исчезнуть настолько, что линия потеряет читаемый endpoint.

effectiveTarget = intersection( stylePreset.bandForVisualRole(visualRole), functionalVisibilityFloor(functionalRole), paletteConstraints, globalColorHierarchy? // DEFERRED ) if intersection is empty: emit COLOR_TARGET_CONFLICT do not invent compromise silently

Тональная часть: вычисляется строго

02 / standards-based math

sRGB → linear RGB

c = channel / 255 if c ≤ 0.04045: c_lin = c / 12.92 else: c_lin = ((c + 0.055)/1.055)^2.4
DERIVED

Relative luminance

Y = 0.2126*R_lin + 0.7152*G_lin + 0.0722*B_lin 0 ≤ Y ≤ 1
DERIVED

Luminance contrast ratio

Y_hi = max(Yp,Yb) Y_lo = min(Yp,Yb) CR = (Y_hi + 0.05) / (Y_lo + 0.05) 1 ≤ CR ≤ 21
DERIVED
Критическая оговорка: contrast-ratio — полезный математический индикатор различия яркости, но не модель визуального внимания и не готовая композиционная оценка. Порог 4.5:1 нельзя переносить в этот модуль как универсальный «дизайнерский закон». Здесь CR — только один измеряемый вход.

Цветовая часть: OKLab, не RGB distance

03 / perceptual color coordinate

Каноническое внутреннее пространство

Для локальной цветовой дистанции храним цвет в OKLab. Евклидова дистанция используется как инженерная приблизительно-перцептивная мера и не объявляется универсальным порогом восприятия.

pointOK = (Lₚ, aₚ, bₚ) bgOK = (Lᵦ, aᵦ, bᵦ) dOK = sqrt( (Lₚ-Lᵦ)² + (aₚ-aᵦ)² + (bₚ-bᵦ)² )
GEOMETRIC DISTANCE IN OKLAB

Нормализация дистанции

// not a universal perceptual law // calibratable mapping colorN = clamp( dOK / stylePreset.oklabRefDistance, 0, 1 ) oklabRefDistance = STYLE_PRESET / INITIAL_HEURISTIC

Мы специально не фиксируем «магическое» значение дистанции в core. Оно должно калиброваться по корпусу реальных композиций.

Фон измеряется там, где находится точка

04 / local sampling

Flat background

if background is analytically flat: bgColor = background.color complexity = 0 no raster sampling required

Raster / texture

sampleRadiusPx = max( kSample * point.radiusPx, minSampleRadiusPx ) sample annulus/disk under future point footprint

HEURISTIC kSample и minimum radius — калибруемые параметры.

Robust statistics

collect luminance samples Y[] Y10 = percentile(Y,10) Y50 = percentile(Y,50) Y90 = percentile(Y,90) MAD = median(|Y-Y50|) complexity = normalize(MAD)

Медиана устойчивее среднего к единичным бликам/пикселям.

tonalWorstCase = min( contrastRatio(pointY, Y10), contrastRatio(pointY, Y50), contrastRatio(pointY, Y90) ) // If background complexity is high: // local metric confidence decreases. // Do not hide this uncertainty. confidence = 1 - backgroundComplexityPenalty
Не усреднять большой участок изображения. Средний RGB может создать цвет, которого реально нет под объектом. Для малого point-элемента оценивается именно локальный footprint.

Канонический localContrast

05 / owned metric

1. Тон нормализуется

// mapping shape = HEURISTIC toneN = clamp( ln(CR_worst) / ln(21), 0, 1 )

CR = 1 → 0; CR = 21 → 1.

2. Цвет нормализуется

colorN = normalizeOKLab(dOK) 0 = locally same color 1 = high normalized color distance

3. Объединяем

localContrast = wTone * toneN + wColor * colorN wTone+wColor = 1 weights = STYLE_PRESET

По умолчанию tone должен иметь больший вес, но конкретное отношение не является универсальным законом.

Важно: Point.localContrast — результат именно этого модуля. Compo 1.01 получает его и самостоятельно вычисляет contrastModifier = f(localContrast). Так нет двойного scoring и циклической зависимости.

Одинаковая геометрия — разные локальные метрики

06 / visual proof
LOW TONE / LOW COLOR
Точка почти сливается с полем: geometry неизменна, localContrast низкий.
HIGH TONAL CONTRAST
Та же площадь, но tonalContrast резко выше.
TONE + CHROMA RELATION
Цветовая дистанция считается отдельно и затем входит в localContrast по preset-весам.

Если цвет не задан — генерация кандидатов

07 / local color candidate engine
for color in palette.allowedColors: metrics = measureLocalContrast(color, backgroundStats) if violatesHardPaletteConstraint(color): reject if metrics.localContrast < functionalVisibilityFloor(functionalRole): reject_or_defer_to_outline_strategy visualFit = bandFit( metrics.localContrast, visualRoleTargetBand(visualRole) ) stability = contrastStabilityAcrossPatch(color, backgroundStats) localScore = w_visualFit * visualFit + w_stability * stability + w_paletteFit * paletteFit(color) - w_complexity * backgroundComplexity return ranked local candidates // Global color hierarchy is DEFERRED_INPUT. // A locally best color is not automatically final.

Интерактивная лаборатория

08 / executable example
LOCAL METRICS · EXACT JS MATH toneNcolorNlocalContrast
Здесь используется реальная конвертация sRGB → linear RGB → OKLab. Никакого AI и никакого RGB-distance surrogate.

Порядок работы

09 / no-cycle pipeline
1 INPUT
roles, point geometry, palette
2 SAMPLE
local background footprint
3 MEASURE
Y, CR, OKLab, complexity
4 NORMALIZE
toneN, colorN
5 LOCAL CONTRAST
owned metric [0..1]
6 CANDIDATES
if color is mutable
7 DEFER GLOBAL
color hierarchy / dominance
8 RETURN
1.01 recomputes weight
RECOMPUTE RULE rerun Compo 1.10 when: point.position changes point.radius changes enough to alter footprint point.color changes underlying background changes palette/style policy changes then: if |localContrast_new - localContrast_old| > epsilonContrast: notify Compo 1.01 recompute visualWeightContextual max local corrective passes = engine policy not owned here

Детерминированные тесты

10 / numeric regression
CasePointBackgroundExpected luminance CRЧто проверяем
T01#FFFFFF#00000021.0000верхняя граница contrast ratio
T02#111111#F5F2E8≈16.8586высокое тональное разделение
T03#C6C2B8#D8D4CA≈1.2015очень слабое тональное разделение
T04#EF372E#111111≈4.7262CR измеряется независимо от perceptual color distance
T05#2639FF#20C7BF≈3.2083OKLab-distance должна считаться отдельно от CR
T06same colorsame color1.0000toneN=0, color distance = 0, localContrast = 0

API TEST

1.10 MUST provide: Point.localContrast 1.10 MUST NOT provide: Point.visualWeightContextual contrastModifier

ROLE TEST

functionalRole = anchor visualRole = background expected: roles remain orthogonal functional floor may raise minimum visibility

COMPLEX BG TEST

high MAD / wide Y10..Y90 expected: backgroundComplexity ↑ metric confidence ↓ no fake certainty

Data for layout engine

11 / canonical machine block
{ "module": "Compo 1.10", "version": "2.0", "scope": "point local color and tone relation", "consumes": { "Point.visualRole": "Compo 1.00", "Point.functionalRole": "Compo 1.00", "Point.subroles": "Compo 1.00", "Point.radiusShortN": "Compo 1.01", "Point.position": "placement pipeline", "Scene.backgroundSampler": "engine", "Scene.paletteConstraints": "style/system", "Global.colorHierarchy": "DEFERRED_INPUT" }, "owns_metrics": [ "point.local_contrast", "point.local_tonal_contrast", "point.local_oklab_distance", "point.background_complexity", "point.color_separation_score", "point.color_candidate_fit" ], "provides": { "Point.localContrast": "float_0_1", "Point.tonalContrastRatio": "float_1_21", "Point.tonalContrastN": "float_0_1", "Point.oklabDistance": "float_gte_0", "Point.backgroundComplexity": "float_0_1", "Point.colorSeparationScore": "float_0_1", "Point.colorCandidates": "array", "Point.colorDiagnostics": "object" }, "must_not_write": [ "Point.visualWeightBase", "Point.visualWeightContextual", "contrastModifier", "global.colorHierarchyScore" ], "math_status": { "srgb_linearization": "DERIVED_STANDARDS_BASED", "relative_luminance": "DERIVED_STANDARDS_BASED", "luminance_contrast_ratio": "DERIVED_STANDARDS_BASED", "oklab_coordinates": "DERIVED_FROM_COLOR_TRANSFORM", "oklab_euclidean_distance": "DERIVED_DISTANCE_NOT_UNIVERSAL_PERCEPTUAL_THRESHOLD", "tone_normalization": "INITIAL_HEURISTIC", "tone_color_weights": "STYLE_PRESET", "role_target_bands": "STYLE_PRESET_OR_INITIAL_HEURISTIC", "background_complexity_mapping": "INITIAL_HEURISTIC" }, "role_model": { "visualRole": ["focus","support","background"], "functionalRole": ["free","anchor","node","rhythm_member","marker"], "rule": "visual role sets target band; functional role may set visibility floor" }, "pipeline": [ "sample_local_background", "compute_luminance_statistics", "compute_oklab_statistics", "compute_owned_local_metrics", "generate_color_candidates_if_mutable", "hard_filter_palette_and_visibility", "rank_local_candidates", "defer_global_color_hierarchy", "provide_point.localContrast_to_Compo1.01" ], "recompute_when": [ "position changes", "radius changes", "point color changes", "underlying background changes", "palette/style policy changes" ], "finality": "LOCAL_MEASUREMENT_AND_PROPOSAL_ONLY" }

Что было исправлено

12 / migration notes

ROLE TAXONOMY

focus/support/background отделены от anchor/node/marker.

OWNERSHIP

Убраны colorAttentionModifier и запись visualWeight.

COLOR MATH

Интерактивный пример больше не использует RGB-distance; используется OKLab.

WCAG RATIO

Contrast ratio оставлен только как математическая luminance-метрика, а не как универсальная модель внимания.