COMPO · 2.17 · LINE RHYTHM / STROKE PATTERN

Ритм линий
и stroke-
pattern.

Модуль описывает повторяемость и вариацию внутри одной линии и внутри локальной группы линий: dash/gap pattern, phase, duty cycle, интервалы между линиями, регулярный, чередующийся и прогрессивный ритм. Он измеряет структуру последовательности, но не считает регулярность универсально «красивее» нерегулярности.

API-контракт

00 / ownership

CONSUMES

Line[] Line.role Line.subrole? Line.pathGeometry Line.dashArray? Line.dashOffset? Line.strokeWidthPx? OPTIONAL: Compo2.12.ParallelGroupDescriptor Compo2.12.offsets[] Compo2.12.gaps[] Compo2.15.MotifGroupDescriptor Compo2.15.raw gaps/order StylePreset.lineRhythm?

PROVIDES

StrokePatternDescriptor LineRhythmDescriptor RhythmModelFit[] RhythmEvent[] RhythmCandidate[] RhythmCorrectionRequest[] line.rhythm.patternFit line.rhythm.phaseCoherence line.rhythm.intervalModelFit line.rhythm.variationFit line.rhythm.localRhythmScore line.rhythm.confidence

DOES NOT OWN

line visual weight // 2.06 parallel grouping geometry // 2.12 graphic grammar // 2.15 line color // 2.16 curvature tension // 2.18 global composition rhythm // Topic 26 global hierarchy/balance global eye-flow
Два уровня: 2.17 работает с intra-line rhythm (dash/gap вдоль path) и local inter-line rhythm (интервалы внутри уже определённой line/motif group). Общий ритм всей композиции остаётся отдельной глобальной темой.

Stroke pattern

01 / pattern along arc length

Canonical pattern

StrokePatternDescriptor { dashArrayPx[], normalizedDashArray[], cycleLengthPx, dashOffsetPx, phase01, inkDutyCycle, segmentCountPerCycle, patternClass, confidence }

Cycle length

C = Σ pattern_i if CSS-like dash list has odd number of values: duplicate list once before canonicalization

После canonicalization цикл всегда имеет чёткое dash/gap чередование.

Duty cycle

inkDutyCycle = Σ dashLengths / cycleLength dash indices: 0,2,4,... gap indices: 1,3,5,...

inkDutyCycle — descriptor. 2.06 может потреблять его при пересчёте effective visual weight.

normalizePattern(pattern): 1. reject negative values 2. if empty → SOLID 3. if odd length → concatenate(pattern, pattern) 4. C = sum(pattern) 5. if C <= eps → INVALID / SOLID_BY_RENDERER_POLICY 6. normalized_i = pattern_i / C IMPORTANT: pattern proportions are independent from absolute cycle size. [20,10] and [40,20] have same normalized pattern, but different spatial frequency.

Phase и синхронизация

02 / cyclic alignment

Normalized phase

phasePx = positiveModulo( dashOffsetPx, cycleLengthPx ) phase01 = phasePx / cycleLengthPx

Знак dashOffset должен следовать renderer contract. Внутри 2.17 хранится одна каноническая convention.

Circular distance

d = abs(phaseA - phaseB) phaseDistance = min(d, 1-d) phaseFit = 1 - clamp( phaseDistance / phaseTau, 0,1 )

DERIVED + HEURISTIC τ Учитывает wrap-around: 0.98 и 0.02 близки, а не далеки.

Phase applicability: сравнивать phase имеет смысл только у совместимых pattern families. Для разных cycle structures phaseCoherence=N/A, если StylePreset не определяет явное отношение.

Конец path и обрезанный цикл

03 / terminal rhythm

Cycle remainder

usable = pathLengthPx + phaseShiftPx remainder = positiveModulo( usable, cycleLengthPx )

Terminal fragment

evaluate final visible dash/gap fragment terminalFragmentRatio = fragmentLength / nominalSegmentLength

Очень маленький случайный фрагмент может выглядеть как rendering artifact.

Repair options

SHIFT_PHASE SCALE_CYCLE FIT_INTEGER_CYCLES KEEP_AS_IS selected by StylePreset policy
Не принуждать: «целое число циклов на path» — не универсальная норма. Это один из style modes. Иногда намеренно обрезанный dash создаёт нужное напряжение.

Ритм группы линий

04 / ordered interval sequence

Input order

preferred: Compo2.12 sorted offsets[] gaps_i = offset[i+1] - offset[i] or: Compo2.15 motif order + raw positions 2.17 must not rebuild group membership.

Normalize gaps

scale = median(gaps) gapN_i = gaps_i / scale if median <= eps: invalid / overlapping defer geometry repair

Нормализация позволяет сравнивать ритм при равномерном масштабировании группы.

LineRhythmDescriptor { groupId, orderedMemberIds[], gapsPx[], gapsN[], medianGapPx, robustGapSpread, modelFits[], selectedModel, variationEvents[], phaseCoherence?, localRhythmScore, confidence }

Модели ритма

05 / model fitting
A A A
regular
g_i ≈ μ error = robustResidual( g_i - μ )
A B A
alternating
g_even ≈ a g_odd ≈ b period = 2 residual normalized by median gap
↗
progressive
linear: g_i ≈ a + b*i optional geometric: log g_i ≈ a+b*i if g_i > 0
≋
controlled irregular
target distribution / bounded deviations / explicit event pattern requires preset or semantic intent
RhythmModelFit { model: REGULAR | ALTERNATING | PROGRESSIVE_LINEAR | PROGRESSIVE_GEOMETRIC | CONTROLLED_IRREGULAR | FREE, normalizedResidual, fitScore, parameters, applicability, complexity, confidence } selectedModel = best applicable model under StylePreset / semantic target. Lowest residual alone does NOT define aesthetic preference.

Вариация и акцент

06 / deviation as event

Baseline residual

r_i = observed_i - predicted_i rN_i = r_i / medianGap

Residual показывает отклонение от выбранной модели.

Rhythm event

RhythmEvent { index, type: ACCENT_GAP | COMPRESSED_GAP | PHASE_SHIFT | PATTERN_CHANGE, magnitude, intentional?, provenance }

Variation budget

StylePreset: maxEvents? eventMagnitudeRange? allowedIndices? periodicBreaks? randomBudget? variationFit = fit(events,target)
Отклонение ≠ ошибка: единичный большой gap может быть syncopation/accent, а не дефект. Без style/semantic target 2.17 обязан хранить его как descriptor/event и снижать уверенность в интерпретации, а не автоматически штрафовать.

Pattern frequency

07 / absolute vs normalized rhythm

Normalized structure

[20,10] → [0.667,0.333] [40,20] → [0.667,0.333] same proportions

Absolute frequency

cyclesPer100Px = 100 / cycleLengthPx cyclesPerPath = pathLengthPx / cycleLengthPx

Scale-relative

cycleShortN = cycleLengthPx / canvasShortSidePx or relative to: strokeWidthPx font size module size

Reference scale должен быть указан явно.

Local rhythm scoring

08 / ownership

OWNS

line.rhythm.patternFit line.rhythm.phaseCoherence line.rhythm.intervalModelFit line.rhythm.variationFit line.rhythm.localRhythmScore line.rhythm.confidence descriptors: cycleLengthPx normalizedDashArray[] inkDutyCycle gapsPx[] gapsN[] modelFits[] RhythmEvent[]

DOES NOT OWN

line.visualWeight line.parallel.localGroupCoherence line.graphic.familyConsistency line.color.localColorScore line.curvature.tension global.compositionRhythm global.hierarchy global.balance global.density
localRhythmScore = weightedMeanApplicable([ patternFit?, phaseCoherence?, intervalModelFit?, variationFit? ]) BUT: each component must be compared to an explicit target profile. If no target rhythm exists: return descriptors localRhythmScore = N/A OR score only hard functional consistency explicitly requested by role.
Главное: 2.17 измеряет «насколько последовательность соответствует выбранной ритмической модели», а не «насколько она регулярна». Это защищает генератор от превращения всех композиций в механически равномерные.

Correction requests

09 / controlled changes

ADJUST GAP

Сдвинуть одну/несколько линий в допустимом диапазоне.

ADJUST PHASE

Синхронизировать dash phase внутри совместимой группы.

ADJUST CYCLE

Изменить dash/gap proportions или absolute cycle length.

INSERT EVENT

Добавить осознанный break/accent только если это разрешено target profile.

RhythmCorrectionRequest { targetIds[], reasonCode: PATTERN_OUT_OF_TARGET | PHASE_MISMATCH | INTERVAL_MODEL_MISMATCH | UNINTENTIONAL_OUTLIER | TERMINAL_FRAGMENT_TOO_SMALL | MISSING_VARIATION_EVENT, targetOwner: "LINE_STYLE" | "Compo2.12" | "Compo2.15" | "GLOBAL_SOLVER" | "STYLE_PRESET", requestedChange, allowedRange?, confidence }

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

10 / rhythm cases
PHASE FAMILY: одинаковый pattern и одинаковая phase создают синхронную локальную систему.
REGULAR: gaps 50/50/50/50. Это хороший fit для REGULAR target, но не универсальный эталон.
ALTERNATING: gaps 40/60/40/60/40. Низкий fit REGULAR, высокий fit period-2.
Progressive rhythm: gaps растут примерно 45→60→75→90→105. 2.17 может fit линейную прогрессию и вернуть slope/residual.
Одинаковая структура pattern, но разные phases. Если target требует синхронизации — это mismatch; если target допускает phase progression — это может быть intentional rhythm.

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

11 / regression

TEST 01 · odd dash list

input=[10,5,2] canonical: [10,5,2,10,5,2] expected: cycle=34

TEST 02 · duty cycle

pattern=[20,10] expected: cycle=30 inkDutyCycle=2/3

TEST 03 · phase wrap

phaseA=.98 phaseB=.02 expected: circularDistance=.04 not .96

TEST 04 · regular

gaps=[50,50,50,50] expected: regular residual=0

TEST 05 · alternating

gaps=[40,60,40,60,40] expected: period-2 residual=0 regular residual>0

TEST 06 · progressive

gaps=[45,60,75,90,105] expected: linear fit: slope=15 residual≈0

TEST 07 · scale invariance

gaps A=[20,40,20] gaps B=[40,80,40] expected: same normalized interval pattern

TEST 08 · no target

valid irregular gaps no style target expected: descriptors returned localRhythmScore=N/A

TEST 09 · ownership

change line color only expected: 2.17 rhythm metrics unchanged

LIVE REFERENCE TESTS

IDInputComputedStatus

DATA FOR LAYOUT ENGINE

12 / machine contract
{ "module": "Compo 2.17", "name": "line_rhythm_and_stroke_pattern", "version": "1.0", "scope": "LOCAL_LINE_RHYTHM", "consumes": [ "Compo2.00.Line[]", "Compo2.00.LinePathGeometry", "Line.dashArray?", "Line.dashOffset?", "Line.strokeWidthPx?", "Compo2.12.ParallelGroupDescriptor?", "Compo2.12.offsets[]?", "Compo2.12.gaps[]?", "Compo2.15.MotifGroupDescriptor?", "Compo2.15.rawGaps?", "StylePreset.lineRhythm?" ], "provides": [ "StrokePatternDescriptor", "LineRhythmDescriptor", "RhythmModelFit[]", "RhythmEvent[]", "RhythmCandidate[]", "RhythmCorrectionRequest[]" ], "rhythm_models": [ "REGULAR", "ALTERNATING", "PROGRESSIVE_LINEAR", "PROGRESSIVE_GEOMETRIC", "CONTROLLED_IRREGULAR", "FREE" ], "owns_metrics": [ "line.rhythm.patternFit", "line.rhythm.phaseCoherence", "line.rhythm.intervalModelFit", "line.rhythm.variationFit", "line.rhythm.localRhythmScore", "line.rhythm.confidence" ], "descriptors_only": [ "cycleLengthPx", "normalizedDashArray[]", "inkDutyCycle", "phase01", "cyclesPerPath", "gapsPx[]", "gapsN[]", "modelFits[]", "RhythmEvent[]", "terminalFragmentRatio" ], "metric_policy": { "primary_vote": "line.rhythm.localRhythmScore", "regularity": "NOT_MONOTONICALLY_BETTER", "target_required_for_aesthetic_fit": true, "no_target": "RETURN_DESCRIPTORS_AND_NA_SCORE", "components": "diagnostic_only_by_default" }, "initial_heuristics": { "phase_tau": { "value": 0.08, "source": "INITIAL_HEURISTIC" }, "terminal_fragment_min_ratio": { "value": 0.20, "source": "STYLE_DEPENDENT_INITIAL_HEURISTIC" }, "regular_residual_tau": { "value": 0.12, "source": "INITIAL_HEURISTIC" }, "variation_event_threshold_mad": { "value": 2.5, "source": "IMPLEMENTATION_HEURISTIC" } }, "hard_rules": [ "odd_dash_array_must_be_canonicalized_before_cycle_analysis", "dash_pattern_values_must_not_be_negative", "phase_distance_must_be_circular", "group_membership_must_be_consumed_from_group_owner_not_rebuilt_here", "rhythm_model_fit_must_not_assume_regular_is_best", "no_explicit_target_means_no_universal_aesthetic_rhythm_score", "line_color_changes_must_not_change_owned_rhythm_metrics", "line_visual_weight_remains_owned_by_2.06", "global_composition_rhythm_remains_outside_2.17", "scale_normalized_and_absolute_frequency_descriptors_must_be_kept_separate" ], "deferred_inputs": [ "Compo2.18.curvatureTension", "global.compositionRhythm", "global.hierarchyScore", "global.balanceScore", "global.densityScore", "global.eyeFlowScore" ], "recompute_when": [ "dashArray changes", "dashOffset changes", "LinePathGeometry/pathLength changes", "2.12 group offsets or gaps change", "2.15 motif membership/order changes", "StylePreset.lineRhythm changes" ], "finality": "LOCAL_LINE_RHYTHM_ONLY" }
ARCHITECTURE CHECKPOINT: 2.17 отделяет ритм от простого равенства интервалов. Regular, alternating, progressive и controlled-irregular — разные модели, а не шкала «хорошо → плохо». Следующий Compo 2.18 должен заниматься кривизной и напряжением линии, не переоценивая уже принадлежащий 2.08 directional flow.
VISITS / ...