API-контракт
00 / ownership
CONSUMES
Line
Line.role
Line.subrole?
Line.strokeColor
Line.opacity
Line.blendMode?
Line.pathGeometry
Line.strokeWidthPx?
RenderedBackgroundSampler
Canvas.colorProfile?
StylePreset.lineColor?
OPTIONAL:
Compo2.06.contextualWeight
Compo2.15.graphicKind
ProtectedContrastTargets[]
PROVIDES
LineColorDescriptor
LineContrastDescriptor
LineAppearanceProfile
LineColorCandidate[]
ColorCorrectionRequest[]
line.color.localContrast
line.color.contrastUniformity
line.color.roleColorFit
line.color.opacityFit
line.color.localColorScore
line.color.confidence
DOES NOT OWN
line.visualWeight // 2.06
line.roleLengthFit // 2.07
line.flow.localCue // 2.08
line.graphic.functionFit // 2.15
line.rhythm.* // 2.17
global.colorHierarchy
global.paletteHarmony
global.balance
global.eyeFlow
Ключевой принцип: исходный HEX/RGB — не достаточная характеристика линии. Одна и та же линия после alpha compositing может иметь совершенно разный rendered tone и contrast на разных участках изображения.
Цветовые представления
01 / measurement spaces
sRGB
Input/rendering interchange. Нельзя напрямую использовать sRGB-каналы для физически корректного luminance.
Linear RGB
Нужен для alpha compositing и relative luminance.
OKLab / OKLCH
Удобен для perceptual lightness, chroma и hue-distance в style matching.
Relative luminance
Скалярная тональная величина для contrast calculations.
sRGB channel c ∈ [0,1]
linearize(c):
if c <= 0.04045:
c_lin = c / 12.92
else:
c_lin = ((c + 0.055) / 1.055)^2.4
relative luminance:
Y = 0.2126 R_lin
+ 0.7152 G_lin
+ 0.0722 B_lin
// coefficients are STANDARD, not heuristic.
Прозрачность и compositing
02 / effective rendered color
Normal alpha blend
Given:
foreground linear RGB F
background linear RGB B
alpha a ∈ [0,1]
C_eff =
a * F +
(1-a) * B
DERIVED Сначала compositing в linear-light модели, затем при необходимости перевод обратно.
Opacity semantics
opacity = 0:
invisible
opacity = 1:
fully opaque
effectiveContrast
must be measured AFTER
compositing against
actual local background
Opacity не оценивается изолированно как «хорошая/плохая».
Blend modes: multiply/screen/overlay и другие режимы требуют соответствующей compositing function. Если движок её не поддерживает точно, контраст помечается approximate и confidence снижается.
Контраст линии
03 / rendered separation from background
Luminance ratio
Y_hi = max(Y_line,Y_bg)
Y_lo = min(Y_line,Y_bg)
contrastRatio =
(Y_hi + 0.05) /
(Y_lo + 0.05)
STANDARD Полезный descriptor, но не универсальный эстетический score.
ΔL
toneDifference =
abs(
L_oklab(line) -
L_oklab(bg)
)
Perceptual lightness difference удобно использовать отдельно от hue/chroma.
Color distance
colorDistance =
distanceOKLab(
C_line,
C_bg
)
Хроматическое различие может поддерживать заметность даже при близкой luminance.
Не смешивать: luminance contrast, perceptual color distance и opacity — независимые descriptors. Composite local contrast должен явно указывать, какие каналы использованы.
Фон вдоль линии
04 / arc-length sampling
Sampling
for q_i uniformly spaced
by ARC LENGTH:
P_i = sampleAtArcFraction(q_i)
background_i =
sampleRenderedBackground(
P_i,
neighborhoodRadius
)
lineEffective_i =
composite(
strokeColor,
opacity,
background_i
)
Используем тот же arc-length contract, что и другие path-модули.
Profile
contrast_i =
contrast(lineEffective_i,
background_i)
profile:
mean
median
min
max
std
p10
p90
Минимум нужен для выявления локальных провалов читаемости.
contrastUniformity =
1 - clamp(
robustSpread(contrast_i) /
contrastSpreadTau,
0, 1
)
backgroundComplexity:
descriptor from sampler/context
NOT owned by 2.16
sample count:
geometry / implementation policy
should scale with pathLengthPx
and background spatial frequency.
Sample invariance: среднее не должно зависеть от того, как geometry разбита на segments. Samples распределяются по arc length, а не «по одному на сегмент».
Цвет относительно роли
05 / roleColorFit
| Role / context | Что может требовать preset | Что нельзя считать универсальным правилом |
| divider | низкий/средний контраст, если divider должен быть вторичным | «divider всегда серый» |
| connector | достаточная различимость пути и endpoints | «connector всегда ярче объектов» |
| accent | высокая цветовая/тональная заметность | «accent обязательно самый насыщенный» |
| annotation | читаемость + соответствие technical palette | «аннотация должна быть чёрной» |
| texture | ограниченный contrast envelope | «texture должна быть едва видимой» |
| frame | контраст относительно содержимого и background | «frame должен доминировать» |
RoleColorTarget {
role,
subrole?,
contrastRange?,
lightnessRange?,
chromaRange?,
hueFamilies?,
opacityRange?,
backgroundDependent?,
provenance:
STYLE_PRESET |
EMPIRICAL |
HEURISTIC
}
roleColorFit =
fitMeasuredAppearanceToTarget(...)
Contrast floors и ceilings
06 / function-aware thresholds
Floor
if role requires visibility:
localContrast
should not fall below
required minimum
minimum comes from:
semantic policy
accessibility policy
StylePreset
empirical calibration
Ceiling
if role is subordinate:
excessive contrast
may exceed target envelope
this is ROLE FIT,
not generic contrast penalty
N/A
if no target profile
exists:
roleColorFit = N/A
DO NOT invent
a universal preferred
contrast.
Local color scoring
07 / ownership
OWNS
line.color.localContrast
line.color.contrastUniformity
line.color.roleColorFit
line.color.opacityFit
line.color.localColorScore
line.color.confidence
descriptors:
effectiveColorProfile
luminanceProfile
lightnessProfile
colorDistanceProfile
contrastRatioProfile
DOES NOT OWN
line.visualWeight
global.colorHierarchy
global.paletteHarmony
global.accentDistribution
global.balance
global.clutter
global.eyeFlow
text readability
image semantic contrast
localColorScore =
weightedMeanApplicable([
roleColorFit,
opacityFit,
requiredVisibilityFit?,
contrastUniformity?
])
IMPORTANT:
raw contrast is usually a DESCRIPTOR,
not automatically a positive vote.
More contrast ≠ always better.
Metric dependency: если localColorScore уже содержит roleColorFit/opacityFit, Compo 2.20 должен голосовать composite-first и не суммировать дочерние метрики второй раз.
Correction requests
08 / bounded changes
CHANGE COLOR
Сдвинуть L/C/H в разрешённом preset диапазоне.
CHANGE OPACITY
Изменить alpha, сохраняя role/style constraints.
LOCAL VARIANT
Если фон неоднороден — предложить сегментный/адаптивный color treatment, если стиль допускает.
REQUEST BACKDROP
Запросить подложку/halo у внешнего owner, а не менять геометрию линии самовольно.
ColorCorrectionRequest {
lineId,
reasonCode:
BELOW_VISIBILITY_FLOOR |
ABOVE_ROLE_CONTRAST_CEILING |
LOW_CONTRAST_UNIFORMITY |
OPACITY_OUT_OF_TARGET |
HUE_OUT_OF_STYLE |
UNSUPPORTED_BLEND_MODE,
targetOwner:
"LINE_STYLE" |
"STYLE_PRESET" |
"GLOBAL_SOLVER" |
"BACKGROUND_OWNER",
requestedChange,
allowedRange?,
confidence
}
Визуальная лаборатория
09 / appearance cases
Детерминированные тесты
10 / regression
TEST 01 · black/white
line=#000
bg=#fff
opacity=1
expected:
contrastRatio=21
TEST 02 · transparent black
line=#000
bg=#fff
alpha=0.5
expected:
effective color
computed after
linear compositing
TEST 03 · opacity zero
alpha=0
expected:
effectiveColor=background
contrastRatio=1
TEST 04 · local background
same stroke
two backgrounds
expected:
two different
local contrasts
TEST 05 · no universal max
accent target:
high contrast
texture target:
low contrast
expected:
roleColorFit differs
for same measured contrast
TEST 06 · N/A
no role target
expected:
roleColorFit=N/A
MUST NOT default
to 1 or 0
TEST 07 · sampling
same path geometry
different segmentization
expected:
same arc-length
contrast profile
within tolerance
TEST 08 · ownership
change line width
without color/bg change
expected:
color descriptors stable
except neighborhood effects
explicitly configured
TEST 09 · global leak
global hierarchy changes
expected:
2.16 owned metrics
unchanged unless
role target input changes
DATA FOR LAYOUT ENGINE
11 / machine contract
{
"module": "Compo 2.16",
"name": "line_color_tone_opacity",
"version": "1.0",
"scope": "LOCAL_LINE_COLOR_APPEARANCE",
"consumes": [
"Compo2.00.Line",
"Compo2.00.Line.role",
"Compo2.00.Line.subrole?",
"Compo2.00.LinePathGeometry",
"Line.strokeColor",
"Line.opacity",
"Line.blendMode?",
"RenderedBackgroundSampler",
"Canvas.colorProfile?",
"Compo2.06.contextualWeight?",
"Compo2.15.graphicKind?",
"StylePreset.lineColor?"
],
"provides": [
"LineColorDescriptor",
"LineContrastDescriptor",
"LineAppearanceProfile",
"LineColorCandidate[]",
"ColorCorrectionRequest[]"
],
"owns_metrics": [
"line.color.localContrast",
"line.color.contrastUniformity",
"line.color.roleColorFit",
"line.color.opacityFit",
"line.color.localColorScore",
"line.color.confidence"
],
"descriptors_only": [
"effectiveColorProfile",
"luminanceProfile",
"lightnessProfile",
"colorDistanceProfile",
"contrastRatioProfile",
"contrastMin",
"contrastMedian",
"contrastP10",
"contrastP90"
],
"metric_policy": {
"primary_vote": "line.color.localColorScore",
"raw_contrast": "DESCRIPTOR_NOT_MONOTONIC_BEAUTY_SCORE",
"components": "diagnostic_only_by_default",
"role_targets": "STYLE_OR_SEMANTIC_DEPENDENT",
"N/A": "EXCLUDE_FROM_AGGREGATION"
},
"standard_math": {
"srgb_linear_threshold": 0.04045,
"srgb_low_divisor": 12.92,
"srgb_offset": 0.055,
"srgb_scale": 1.055,
"srgb_gamma": 2.4,
"relative_luminance_coefficients": [0.2126, 0.7152, 0.0722],
"contrast_luminance_offset": 0.05
},
"initial_heuristics": {
"contrast_spread_tau": {
"value": 2.0,
"source": "INITIAL_HEURISTIC"
},
"background_sample_radius_px": {
"value": 3,
"source": "IMPLEMENTATION_HEURISTIC"
},
"minimum_samples": {
"value": 16,
"source": "IMPLEMENTATION_HEURISTIC"
}
},
"hard_rules": [
"sRGB_channels_must_be_linearized_before_relative_luminance_and_linear_alpha_compositing",
"effective_contrast_must_be_measured_after_compositing_against_actual_local_background",
"arc_length_sampling_must_not_depend_on_segment_count",
"raw_contrast_must_not_be_treated_as_monotonically_better",
"roleColorFit_must_be_NA_when_no_target_exists",
"color_module_must_not_own_global_palette_harmony_or_color_hierarchy",
"line_visual_weight_remains_owned_by_2.06",
"rhythm_remains_owned_by_2.17",
"unsupported_blend_modes_must_reduce_confidence_or_defer"
],
"deferred_inputs": [
"global.colorHierarchy",
"global.paletteHarmony",
"global.accentDistribution",
"global.balanceScore",
"global.eyeFlowScore",
"Compo2.17.rhythmScore"
],
"recompute_when": [
"strokeColor changes",
"opacity changes",
"blendMode changes",
"background under path changes",
"LinePathGeometry changes",
"role/subrole changes",
"StylePreset.lineColor changes",
"color profile changes"
],
"finality": "LOCAL_COLOR_APPEARANCE_ONLY"
}
ARCHITECTURE CHECKPOINT: 2.16 владеет rendered appearance линии: effective color, tone, opacity, local contrast и соответствием цветовой роли. Он не превращает контраст в универсальный «чем больше, тем лучше» score и не забирает global palette/hierarchy. Следующий Compo 2.17 — ритм линий и stroke-pattern.