§7 · Кривые

Три полноправных типа кривых: Conic (матрица 3×3, 9 ветвей классификации), Function (y = f(x) с sympy-парсингом и автоматическими асимптотами), ImplicitCurve (F(x, y) = 0 через marching squares). Все читают те же ключи стилей, что и линии: stroke, stroke_width_px, fill, z_index.

Три типа кривых

ТипЧто представляетКак рисуетсяfillstroke_dash_ratio
Conicматрица 3×3; 9 ветвей (CIRCLE, ELLIPSE, PARABOLA, HYPERBOLA, 3× lines, POINT, EMPTY)manim-примитив для замкнутых; адаптивный sampler + viewport-clip для парабол/гиперболтолько CIRCLE, ELLIPSEвсе ветви
Functionявная y = f(x), sympy + numpy.lambdifyadaptive sampling с разделением по natural_singularities—да
ImplicitCurveлюбое F(x, y) = 0marching squares 128×128 внутри viewport—да
В отличие от полигонов, у кривых нет собственной JSON-секции (style.curve.* не существует). Они читают canonical defaults через resolver (defaults.function, defaults.conic и т.п.) и перекрываются per-element через elem.style, как обычные линии.
ellipse
Conic (ELLIPSE)
hyperbola
Conic (HYPERBOLA)
y=1/x
Function · асимптота
lemniscate
ImplicitCurve

Conic — конические сечения

Создание

КонструкторКогда используется
Conic(matrix)Сырой 3×3 ndarray — продвинутый случай.
Conic.from_coeffs(a, b, c, d, e, f)По общему уравнению ax² + bxy + cy² + dx + ey + f = 0.
Conic.from_ggb_matrix(A0..A5)Совместимо с GGB XML <matrix>.
Conic("...") / Conic.from_string(eq)DSL: произвольное уравнение, парсится через sympy.

В DSL поддерживается строковая форма:

g1 = Conic("x^2 + y^2 = 4")                # → CIRCLE
g2 = Conic("(x+1)^2/4 + y^2/2 = 1")        # → ELLIPSE
g3 = Conic("y = (x - 0.3)^2")              # → PARABOLA
g4 = Conic("x^2/2 - y^2 = 1")              # → HYPERBOLA
g5 = Conic("x^2 - y^2 = 0")                # → INTERSECTING_LINES

Тип классифицируется лениво при первом обращении к conic.type — по инвариантам ранга матрицы и знака det(M₃₃). Кэшируется.

Эллипс: fill поддерживается

ellipse.py · правьте уравнение; стили — через overrides
ell = Conic("(x+0.3)^2 / 5 + (y-0.2)^2 / 2.2 = 1")

ell.style.fill = '#d6e5f6'
ell.style.fill_opacity = 0.55
ell.style.stroke = '#4d87c6'
ell.style.stroke_width_px = 2.2
Ellipse with fill
Скачать: ellipse.py

Окружность: пунктир через stroke_dash_ratio

Пунктир окружности, как и любой линии, рисуется одним контуром с паттерном (stroke-dasharray в SVG).

circle-dashed.py · попробуйте убрать stroke_dash_ratio
c = Conic("x^2 + y^2 = 6")

c.style.stroke = '#4d87c6'
c.style.stroke_width_px = 2.0
c.style.stroke_dash_ratio = 0.5
Dashed circle
Скачать: circle-dashed.py
stroke_dash_ratio — доля штриха в периоде; период задаёт stroke_dash_period_px (по умолчанию rendering.dash_period_px = 10 px). Пунктир рисуется на всех кривых: окружностях, эллипсах, параболах, гиперболах, графиках функций и неявных кривых; значение 1 и больше или меньше 0 даёт сплошную линию.

Парабола: viewport-clipping

Парабола бесконечна. Чтобы sampler не тратил точки на невидимые участки, аналитически вычисляется диапазон параметра |u| ≤ 2√(p·v_max), где v_max — максимальная проекция оси на видимую область (viewport_t_ranges_parabola в geo/curve_sampling.py).

parabola.py
parab = Conic("y - 0.4 = (x - 0.3)^2 / 1.6")

parab.style.stroke = '#4d87c6'
parab.style.stroke_width_px = 2.2
Parabola
Скачать: parabola.py

Гипербола: две ветви, каждая независимо

Для HYPERBOLA рендер строит две ветви (sign = ±1), для каждой аналитически сужает диапазон параметра до пересечения с viewport (|t| ≤ min(acosh(u_max/a), asinh(v_max/b))), затем вызывает sample_parametric.

hyperbola.py
hyp = Conic("x^2 / 1.8 - y^2 / 1.2 = 1")

hyp.style.stroke = '#4d87c6'
hyp.style.stroke_width_px = 2.2
Hyperbola
Скачать: hyperbola.py

Вырожденные коники

INTERSECTING_LINES, PARALLEL_LINES, DOUBLE_LINE раскладываются на обычные Line-объекты, обрезанные viewport'ом. POINT становится Dot. EMPTY не рисует ничего.

Производные свойства коник

На базе Conic доступны команды:

O   = Center(ell)          # центр
F1  = Focus(hyp)            # первый фокус
V   = Vertex(parab)         # вершина
d   = Directrix(parab)      # директриса (Line)
e   = Eccentricity(hyp)     # эксцентриситет (число)
pol = Polar(P, conic)       # поляра точки
tg  = Tangent(P, conic)     # касательная из внешней точки

Стилизация этих элементов — по их типу (Line/Point), см. §3.

Function — явные y = f(x)

Парсинг

Три эквивалентных способа создать функцию:

# 1. Строковая форма — парсится sympy
f1 = Function("y = x^2 - 3")

# 2. DSL-сугар: f(x) = expr
#    (preprocessor переписывает в Function("y = ..."))
f2(x) = x^2 - 3

# 3. Любое имя переменной — переименование автоматическое
f3(t) = sin(t) / t

Парсер поддерживает:

Переменная берётся из левой части: g(t) = t^2 — график t². Формула может упоминать числа построения — слайдеры, измерения, точки (f(x) = a x^2, y(A) (x - x(B))): график перестраивается при их изменении; логическое значение — условие в If(b, x, -x), а в арифметике — 0 или 1. То же работает для Conic, ImplicitCurve и уравнений прямых Line("y = a x + 1") — и в DSL, и при импорте из GeoGebra ((x - x(A))^2 + (y - y(A))^2 = 4 следует за A). Длина формулы — не более 4000 символов; формулы, требующие огромных вычислений (7^(9^9), (10^7)!), отклоняются.

После парсинга выражение один раз пропускается через sympy.lambdify('numpy'); дальше горячий путь — только numpy-вызовы, без sympy.

Базовый случай

basic.py · попробуйте sin, cos, exp
f(x) = x^2 / 2 - 1

f.style.stroke = '#4d87c6'
f.style.stroke_width_px = 2.2
y = x²/2 − 1
Скачать: basic.py

Асимптоты — автоматическое расщепление

y = 1/x имеет вертикальную асимптоту в x = 0. sympy.singularities находит все такие точки и сохраняет в func.natural_singularities. Рендер делит x-диапазон на подотрезки между соседними singularities, каждый сэмплируется независимо — никакой «соединяющей» линии через бесконечность. У периодической функции (tan, cot, sec, csc) таких точек бесконечно много: берутся те, что не дальше 1000 единиц от начала координат (не больше 1000 в каждом семействе периода).

asymptote.py · попробуйте tan(x) или 1/(x^2 - 1)
f(x) = 1 / x

f.style.stroke = '#f15b5b'
f.style.stroke_width_px = 2.2
y = 1/x
Скачать: asymptote.py
Sampler никогда не вызывает f в самой singularity — отступает на ε = max(width × 1e-3, 1e-6), чтобы избежать ZeroDivisionError. У асимптот кривая визуально обрывается немного раньше теоретической бесконечности — обычно невидимо, но может проявиться при очень высоком zoom.

Piecewise через If[...]

GGB-синтаксис If[-2 ≤ x ≤ 2, x² - 1] переводится в sympy.Piecewise((x² - 1, (x ≥ -2) & (x ≤ 2)), (nan, True)). Вне области — NaN, sampler обрывает полилинию.

piecewise.py
f(x) = If[-2 <= x <= 2, x^2 - 1]

f.style.stroke = '#4d87c6'
f.style.stroke_width_px = 2.5
Piecewise y = x²−1 на [-2, 2]
Скачать: piecewise.py
Альтернатива через Python API. Если функция создаётся из Python (не DSL), можно передать domain=(a, b) прямо в конструктор:
from animageo.geo.lib_function import Function
f = Function('x**2 - 1', domain=(-2, 2))
Рендер пересечёт домен с viewport и сэмплирует только пересечение. DSL-форма Function(...) через putCode не принимает kwargs — для DSL используйте If[...].

Несколько функций, разные стили

trio.py · три гармоники
f(x) = sin(x)
g(x) = sin(2 * x) / 2
h(x) = sin(3 * x) / 3

f.style.stroke = '#4d87c6'
f.style.stroke_width_px = 2.2
g.style.stroke = '#f15b5b'
g.style.stroke_width_px = 1.6
h.style.stroke = '#58c57e'
h.style.stroke_width_px = 1.2
Three sine harmonics
Скачать: trio.py

ImplicitCurve — F(x, y) = 0

Когда нужно

Если кривая не раскрывается как y = f(x) (лемниската, декартов лист, кардиоида) и sympy-классификация не даёт ветви Conic — используется ImplicitCurve. Уравнения вида LHS = RHS нормализуются в LHS − RHS = 0 автоматически.

L = ImplicitCurve("(x^2 + y^2)^2 = 4 * (x^2 - y^2)")   # лемниската
F = ImplicitCurve("x^3 + y^3 = 3 * x * y")             # декартов лист
S = ImplicitCurve("sin(x) + cos(y) = 0.5")             # периодический паттерн

Marching squares

Рендер строит сетку 128×128 внутри viewport, для каждой ячейки смотрит знаки F в 4 углах, по 16-позиционной таблице восстанавливает отрезок контура. Сложность O(grid_n²), память константна. Это значит:

lemniscate.py
L = ImplicitCurve("(x^2 + y^2)^2 = 4 * (x^2 - y^2)")

L.style.stroke = '#4d87c6'
L.style.stroke_width_px = 2.2
Lemniscate
Скачать: lemniscate.py
folium.py · декартов лист
F = ImplicitCurve("x^3 + y^3 = 3 * x * y")

F.style.stroke = '#f15b5b'
F.style.stroke_width_px = 2.0
Folium of Descartes
Скачать: folium.py

Стилевые ключи

Рендер читает тот же набор ключей, что у линий. Дефолты — из StyleConfig + resolver:

КлючТипDefaultПрименим кЭффект
strokehexdefaults.<type>.strokeвсецвет кривой
stroke_width_pxfloat (px)defaults.<type>.stroke_width_pxвсетолщина; рендер: × 100 / ptUnit
stroke_opacity0..11всепрозрачность
stroke_linecapenumrendering.line_capвсеbutt/round/square
stroke_dash_ratio0..1Noneвседоля штриха в периоде; ≤ 0 или ≥ 1 — сплошная
stroke_dash_period_pxfloat (px)rendering.dash_period_px = 10всепериод: штрих + промежуток
fillhexstyle.backgroundтолько CIRCLE / ELLIPSEцвет заливки
fill_opacity0..11только CIRCLE / ELLIPSEпрозрачность заливки
z_indexfloatZ_LINE = 4всеперекрывает автоматику
подписьсм. §6—всеlabel_visible, label_text, label_color, font_size_px, label_offset_px
Не конфигурируется per-element: sample_count, max_samples, grid_n, segment_mu — это глобальные constants в geo/curve_sampling.py. Для большей детализации — уменьшайте px_size / ptUnit (segment_mu станет меньше пропорционально) либо правьте модуль.

Параметры сэмплирования

Все значения фиксированы в коде. Приведены для справки.

ПараметрЗначениеГде используетсяНазначение
segment_mu3.0 / ptUnitConic (parabola, hyperbola), Functionцель адаптивного sampler'a: ≈3 px между соседними точками polyline
initial_samples32sample_parametricстартовая равномерная сетка по t
max_samples500sample_parametricжёсткий cap — защита от зависаний на патологических кривых
grid_n128ImplicitCurveсетка 128×128 ≈ 16K ячеек
viewport_pad0.1 MULiang–Barskyпаддинг, чтобы stroke-cap не попал на край кадра
ε у асимптотыmax(width × 1e-3, 1e-6)Functionотступ от singularity при сэмплировании

Особенности рендера

Пунктир у кривых. Параболы, гиперболы, графики функций и неявные кривые рисуются одной линией с паттерном (stroke-dasharray в SVG); период и долю штриха задают stroke_dash_period_px и stroke_dash_ratio.
Подписи кривых автораскладка не учитывает. compute_label_layout в label_placement.py знает только о точках, сегментах, окружностях, углах. Conic / Function / ImplicitCurve — невидимые для решателя препятствия. В сценах с кривыми лучше отключать автораскладку или фиксировать подписи через label_placement_locked=True.
Viewport-aware — для parabola/hyperbola/function, не для implicit. Implicit curves всегда сэмплируются на сетке 128×128 внутри видимой области — при zoom-in детализация остаётся той же. Parabola/hyperbola/function адаптивны в пикселях (segment_mu ≈ 3 px), поэтому под zoom качество сохраняется.
Пересечения работают с любой парой. Intersect(curve1, curve2) диспатчится через intersect_FK, intersect_Il, intersect_II и т.д. — аналитика там, где возможно (Conic ∩ Line: квадратное уравнение; Conic ∩ Conic: пенсил-метод); numeric fallback (brentq + fsolve Newton) для функций и неявных. Результат — обычные Point-элементы.