COMPO · 2.06 · STROKE WEIGHT SYSTEM

Толщина
и визуальный
вес линии.

Модуль отделяет физическую толщину stroke от воспринимаемого визуального веса. Машина сначала вычисляет базовый вес линии из геометрии, а после размещения получает контекстные модификаторы — контраст, окружение, пересечения и роль — и пересчитывает итоговую заметность.

API-контракт

00 / consumes → provides

CONSUMES

Canvas.widthPx Canvas.heightPx Line.role Line.subrole? Line.rigidity? Line.pathGeometry.pathLengthShortN Line.strokeWidthPx | targetWidth? Line.opacity StylePreset? DEFERRED: localContrast crowding occlusion hierarchyTarget

PROVIDES

Line.widthN Line.widthClass Line.baseWeight Line.contextualWeight Line.weightDelta Line.recomputeRequired Line.weightDescriptor

DOES NOT OWN

global_hierarchy_score negative_space_score eye_flow_score color_contrast_model semantic_importance balance_score

Модуль использует их только как входные модификаторы или цели.

Ключевое правило: толщина — геометрический параметр. Визуальный вес — оценка заметности. Они связаны, но не равны.

Нормализация толщины

01 / deterministic geometry
wPx
physical stroke width

Фактическая толщина в пикселях.

wPx = renderer stroke width
wN
normalized width

Толщина относительно меньшей стороны холста.

wN = wPx / min(W,H)
ρ
width / length ratio

Относительная «массивность» линии.

rho = wPx / max(lengthPx, EPS)
Не использовать только пиксели: линия 8 px на 320×400 и 8 px на 4K-постере — это разные визуальные классы. Все правила генератора должны работать через wN или через масштабируемый preset.

Базовый визуальный вес

02 / intrinsic pass

Base weight

pathLengthFactor = clamp( pow(pathLengthShortN, aL), Lmin, Lmax ) widthFactor = pow(wN / wRef, aW) opacityFactor = pow(opacity, aA) baseWeightMeasured = widthFactor * pathLengthFactor * opacityFactor // role is NOT part of measured weight // role only defines targetWeightRange

HEURISTIC MODEL Коэффициенты должны быть калибруемыми. Формула задаёт устойчивый каркас, а не универсальную психофизическую истину.

Почему длина участвует

При одинаковой толщине линия, занимающая 70% поля, создаёт больше визуальной массы, чем штрих длиной 5% поля.

same widthN: short accent → lower area occupancy long divider → higher area occupancy but: length influence must saturate so XL line does not grow weight infinitely

Поэтому pathLengthFactor должен иметь saturation/clamp.

Measured ≠ desired

02b / anti-circularity

Измеряем

baseWeightMeasured = f( width, pathLength, opacity )

Одинаковая физическая линия имеет один и тот же measured weight независимо от названия role.

Сравниваем с целью

targetWeightRange = profile(role, subrole, hierarchyTarget) weightFit = distance( contextualWeight, targetWeightRange )

Role влияет на желаемый диапазон, но не переписывает измерение.

Контекстный пересчёт

03 / second pass
C
contrast modifier

Приходит из цвето-тонального модуля.

I
isolation modifier

Насколько линия окружена свободным пространством.

O
occlusion modifier

Какую долю линии реально видно.

K
crowding modifier

Насколько её окружение уже насыщено деталями.

contextualWeight = baseWeightMeasured * contrastModifier * isolationModifier * visibleFractionModifier * crowdingModifier weightDelta = contextualWeight - previousContextualWeight if abs(weightDelta) > recomputeThreshold: recomputeRequired = true
Архитектурное разделение: Compo 2.06 не вычисляет контраст, crowding или global hierarchy. Он только принимает их от владельцев и пересчитывает собственную метрику.

Классы толщины

04 / candidate generation
XS
hairline
0.0005 ≤ wN < 0.0015

сеточная, конструкционная, фоновая.

S
thin
0.0015 ≤ wN < 0.004

annotation, connector, technical line.

M
medium
0.004 ≤ wN < 0.012

divider, accent, visible trajectory.

L+
heavy / mass
0.012 ≤ wN ≤ 0.060+

линия становится самостоятельной массой.

INITIAL_HEURISTIC Эти границы не являются законом дизайна. Они нужны для candidate generation и должны калиброваться по типу носителя, DPI, дистанции просмотра и стилю.

Role → target weight

05 / role envelope
RoleBase targetTypical width classRule
baselinevery lowXSобычно construction-only; не конкурировать с текстом
texturelowXS–Sнизкий contrast + высокая повторяемость
annotationlow–midSдостаточно видима для связи, но не доминанта
connectormidS–Mвидимость зависит от длины и числа пересечений
dividermidS–Mдолжен читать разделение, не становясь самостоятельным объектом без причины
axislow–midXS–Mзависит от visibility: construction или visible
trajectorymid–highMдолжна удерживать маршрут, но её вес проверяет eye-flow module
accenthighM–L+может стать локальной доминантой

Визуальная лаборатория

06 / measurable examples
same length / w = 2
Тонкая линия: низкий wN, низкий intrinsic mass.
same length / w = 10
Средняя линия: тот же span, но больший baseWeight.
same length / w = 28
Heavy stroke начинает читаться как геометрическая масса.
same stroke in quiet zone
Высокая isolation: линия заметна сильнее при той же толщине.
same stroke in crowded zone
Высокий crowding: та же линия имеет меньшую относительную заметность.

Подбор толщины по целевому весу

07 / inverse solver

Не угадывать w

Если role/hierarchy задаёт целевой диапазон веса, solver должен подобрать толщину численно.

targetWeight = [Tmin,Tmax] for widthCandidate in widthGrid: base = computeBaseWeightMeasured(widthCandidate, pathLength) if contextAvailable: final = applyContext(base) else: final = base choose width that minimizes: distance(final, targetWeight) + rolePenalty + collisionPenalty

Corrective pass

if contextualWeight > targetMax: try: 1 reduce contrast [other owner] 2 reduce widthN 3 shorten span [Compo 2.07] 4 move to less isolated zone if contextualWeight < targetMin: reverse strategy

Compo 2.06 предлагает width-correction, но не имеет права самостоятельно менять чужие параметры.

Собственные метрики

08 / ownership

OWNS

width_fit = scoreWidthAgainstRoleEnvelope() base_weight_fit = scoreBaseWeightTarget() context_weight_fit = scoreContextualWeightTarget() weight_stability = 1 - clamp(abs(delta)/deltaMax,0,1) localWeightScore = a*width_fit + b*base_weight_fit + c*context_weight_fit + d*weight_stability

DEFERRED

global_hierarchy_score balance_score negative_space_score eye_flow_score semantic_importance color_contrast_score crowding_score

Ни одна из этих метрик не должна дублироваться внутри Compo 2.06.

Context overrides

09 / dependency graph
Source moduleInputEffect on 2.06
Color/Tone futurelocalContrastмодифицирует contextualWeight, но не baseWeight
Hierarchy futuretargetWeightRangeзадаёт цель, под которую 2.06 подбирает width
Intersection / tangency futurevisibleFractionуменьшает perceptual contribution
Negative-space futureisolationModifierможет повысить вес линии в пустой зоне
Texture/rhythm modulescrowdingModifierуменьшает относительную заметность в плотном поле

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

10 / regression

TEST 01 · normalize

canvas = 1080×1350 wPx = 10 shortSide = 1080 expected widthN = 10 / 1080 ≈ 0.00925926

TEST 02 · ordering

same length same opacity same role wA = 0.003 wB = 0.009 expected: baseWeight(B) > baseWeight(A)

TEST 03 · context

baseWeight = 0.50 contrast = 0.80 isolation = 1.10 visible = 0.75 crowding = 0.90 expected contextual ≈ 0.50*0.80*1.10*0.75*0.90 = 0.297

TEST 04 · recompute

previous = 0.41 current = 0.49 threshold = 0.05 delta = 0.08 expected recomputeRequired = true

TEST 05 · no foreign ownership

input hierarchyTarget = 0.70..0.85 2.06 MAY: change width proposal 2.06 MUST NOT: recompute hierarchy_score

Data for layout engine

11 / machine contract
{ "module": "Compo 2.06", "name": "line_width_and_visual_weight", "owner_of": [ "Line.widthN", "Line.widthClass", "Line.baseWeight", "Line.contextualWeight", "Line.weightDelta", "Line.weightRecomputeRequired", "line.width_fit", "line.base_weight_fit", "line.context_weight_fit", "line.weight_stability" ], "consumes": [ "Compo0.Canvas", "Compo2.00.Line.role", "Compo2.00.Line.subrole?", "Compo2.00.Line.rigidity?", "Compo2.00.LinePathGeometry.pathLengthShortN", "Line.strokeWidthPx", "Line.opacity", "StylePreset" ], "deferred_inputs": [ "localContrast", "isolationModifier", "visibleFractionModifier", "crowdingModifier", "hierarchyTargetWeightRange" ], "defaults": { "width_reference_norm": {"value":0.004,"source":"INITIAL_HEURISTIC"}, "width_exponent": {"value":0.85,"source":"INITIAL_HEURISTIC"}, "length_exponent": {"value":0.35,"source":"INITIAL_HEURISTIC"}, "opacity_exponent": {"value":1.0,"source":"INITIAL_HEURISTIC"}, "recompute_threshold": {"value":0.05,"source":"INITIAL_HEURISTIC"} }, "width_classes": { "XS": [0.0005,0.0015], "S": [0.0015,0.0040], "M": [0.0040,0.0120], "L_PLUS": [0.0120,0.0600] }, "pipeline": [ "normalize_width", "compute_base_weight", "classify_width", "receive_context_modifiers_if_available", "compute_contextual_weight", "compare_to_target_range", "propose_width_correction", "flag_recompute_if_delta_exceeds_threshold" ], "finality": "LOCAL_AND_CONTEXTUAL_PROPOSAL_ONLY", "rule": "role/hierarchy set targetWeightRange only; measured weight never contains roleBaseFactor; Compo 2.06 owns width/pathLength/opacity → measuredWeight conversion" }
MIGRATION 2.00–2.07 / pre-2.08: canonical LinePathGeometry, role/subrole/rigidity, metric ownership cleanup, path-length vs spatial-span separation.