Контракт модуля
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
Если цвет не задан — генерация кандидатов
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
Порядок работы
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
| Case | Point | Background | Expected luminance CR | Что проверяем |
| T01 | #FFFFFF | #000000 | 21.0000 | верхняя граница contrast ratio |
| T02 | #111111 | #F5F2E8 | ≈16.8586 | высокое тональное разделение |
| T03 | #C6C2B8 | #D8D4CA | ≈1.2015 | очень слабое тональное разделение |
| T04 | #EF372E | #111111 | ≈4.7262 | CR измеряется независимо от perceptual color distance |
| T05 | #2639FF | #20C7BF | ≈3.2083 | OKLab-distance должна считаться отдельно от CR |
| T06 | same color | same color | 1.0000 | toneN=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-метрика, а не как универсальная модель внимания.