diff --git a/kitty/animation.c b/kitty/animation.c index a1dde6b70..dbb2709e5 100644 --- a/kitty/animation.c +++ b/kitty/animation.c @@ -8,16 +8,27 @@ #include "data-types.h" #define ANIMATION_INTERNAL_API -typedef struct easing_curve_parameters { +typedef struct LinearParameters { size_t count; - double extra0, extra1, extra2, extra3; - const double *params, *positions; -} easing_curve_parameters; + double buf[]; +} LinearParameters; -typedef double(*easing_curve)(easing_curve_parameters*, double); +typedef struct StepsParameters { + size_t num_of_buckets; + double jump_size, start_value; +} StepsParameters; + +static const double bezier_epsilon = 1e-7; +static const int max_newton_iterations = 4; + +typedef struct BezierParameters { + double ax, bx, cx, ay, by, cy, start_gradient, end_gradient, spline_samples[11]; +} BezierParameters; + +typedef double(*easing_curve)(void*, double, monotonic_t); typedef struct animation_function { - easing_curve_parameters params; + void *params; easing_curve curve; double y_at_start, y_size; } animation_function; @@ -42,90 +53,194 @@ animation_is_valid(const Animation* a) { return a != NULL && a->count > 0; } Animation* free_animation(Animation *a) { if (a) { - for (size_t i = 0; i < a->count; i++) free((void*)a->functions[i].params.params); + for (size_t i = 0; i < a->count; i++) free(a->functions[i].params); free(a->functions); free(a); } return NULL; } + static double unit_value(double x) { return MAX(0., MIN(x, 1.)); } static double -linear_easing_curve(easing_curve_parameters *p, double val) { +linear_easing_curve(void *p_, double val, monotonic_t duration UNUSED) { + LinearParameters *p = p_; double start_pos = 0, stop_pos = 1, start_val = 0, stop_val = 1; + double *x = p->buf, *y = p->buf + p->count; for (size_t i = 0; i < p->count; i++) { - if (p->positions[i] >= val) { - stop_pos = p->positions[i]; - stop_val = p->params[i]; + if (x[i] >= val) { + stop_pos = x[i]; + stop_val = y[i]; if (i > 0) { - start_val = p->params[i-1]; - start_pos = p->positions[i-1]; + start_val = y[i-1]; + start_pos = x[i-1]; } break; } } - double frac = (val - start_pos) / (stop_pos - start_pos); - return start_val + frac * (stop_val - start_val); + if (stop_pos > start_pos) { + double frac = (val - start_pos) / (stop_pos - start_pos); + return start_val + frac * (stop_val - start_val); + } + return stop_val; +} + +// Cubic Bezier {{{ +static double +sample_curve_x(const BezierParameters *p, double t) { + // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule. + return ((p->ax * t + p->bx) * t + p->cx) * t; } static double -cubic_bezier_easing_curve(easing_curve_parameters *p, double t) { - const double u = 1. - t, uu = u * u, uuu = uu * u, tt = t * t, ttt = tt * t; - // p0 is start, p3 is end. p1, p2 are control points - return uuu * p->extra0 + 3 * uu * t * p->extra1 + 3 * u * tt * p->extra2 + ttt * p->extra3; +sample_curve_y(const BezierParameters *p, double t) { + return ((p->ay * t + p->by) * t + p->cy) * t; } static double -step_easing_curve(easing_curve_parameters *p, double t) { - double num_of_buckets = p->extra0, start_value = p->extra2, jump_size = p->extra1; - size_t val_bucket = (size_t)(t * num_of_buckets); - return start_value + val_bucket * jump_size; +sample_derivative_x(const BezierParameters *p, double t) { + return (3.0 * p->ax * t + 2.0 * p->bx) * t + p->cx; } +static double +solve_curve_x(const BezierParameters *p, double x, double epsilon) { + // Given an x value, find a parametric value it came from. + double t0 = 0.0, t1 = 0.0, t2 = x, x2 = 0.0, d2 = 0.0; + + // Linear interpolation of spline curve for initial guess. + static const size_t num_samples = arraysz(p->spline_samples); + double delta = 1.0 / (num_samples - 1); + for (size_t i = 1; i < num_samples; i++) { + if (x <= p->spline_samples[i]) { + t1 = delta * i; + t0 = t1 - delta; + t2 = t0 + (t1 - t0) * (x - p->spline_samples[i - 1]) / (p->spline_samples[i] - p->spline_samples[i - 1]); + break; + } + } + + // Perform a few iterations of Newton's method -- normally very fast. + // See https://en.wikipedia.org/wiki/Newton%27s_method. + double newton_epsilon = MIN(bezier_epsilon, epsilon); + for (size_t i = 0; i < max_newton_iterations; i++) { + x2 = sample_curve_x(p, t2) - x; + if (fabs(x2) < newton_epsilon) return t2; + d2 = sample_derivative_x(p, t2); + if (fabs(d2) < bezier_epsilon) break; + t2 = t2 - x2 / d2; + } + if (fabs(x2) < epsilon) return t2; + + // Fall back to the bisection method for reliability. + while (t0 < t1) { + x2 = sample_curve_x(p, t2); + if (fabs(x2 - x) < epsilon) return t2; + if (x > x2) t0 = t2; + else t1 = t2; + t2 = (t1 + t0) * .5; + } + + // Failure. + return t2; +} + +static double +solve_unit_bezier(const BezierParameters *p, double x, double epsilon) { + if (x < 0.0) return 0.0 + p->start_gradient * x; + if (x > 1.0) return 1.0 + p->end_gradient * (x - 1.0); + return sample_curve_y(p, solve_curve_x(p, x, epsilon)); +} + +static double +cubic_bezier_easing_curve(void *p_, double t, monotonic_t duration) { + BezierParameters *p = p_; + // The longer the animation, the more precision we need + double epsilon = 1.0 / monotonic_t_to_ms(duration); + return solve_unit_bezier(p, t, epsilon); +} +// }}} + +static double +step_easing_curve(void *p_, double t, monotonic_t duration UNUSED) { + StepsParameters *p = p_; + size_t val_bucket = (size_t)(t * p->num_of_buckets); + return p->start_value + val_bucket * p->jump_size; +} + +static double +identity_easing_curve(void *p_ UNUSED, double t, monotonic_t duration UNUSED) { return t; } + double -apply_easing_curve(const Animation *a, double val) { +apply_easing_curve(const Animation *a, double val, monotonic_t duration) { val = unit_value(val); if (!a->count) return val; size_t idx = MIN((size_t)(val * a->count), a->count - 1); animation_function *f = a->functions + idx; - double ans = f->curve(&f->params, val); + double ans = f->curve(&f->params, val, duration); return f->y_at_start + unit_value(ans) * f->y_size; } static animation_function* -init_function(Animation *a, double y_at_start, double y_at_end, easing_curve curve, size_t count) { +init_function(Animation *a, double y_at_start, double y_at_end, easing_curve curve) { ensure_space_for(a, functions, animation_function, a->count + 1, capacity, 4, false); animation_function *f = a->functions + a->count++; zero_at_ptr(f); f->y_at_start = y_at_start; f->y_size = y_at_end - y_at_start; f->curve = curve; - if (count) { - double *p = calloc(count*2, sizeof(double)); - if (!p) fatal("Out of memory"); - f->params.params = p; - f->params.positions = p + count; - f->params.count = 0; - } return f; } void -add_cubic_bezier_animation(Animation *a, double y_at_start, double y_at_end, double start, double p1, double p2, double end) { - animation_function *f = init_function(a, y_at_start, y_at_end, cubic_bezier_easing_curve, 4); - f->params.extra0 = start; f->params.extra1 = p1; f->params.extra2 = p2; f->params.extra3 = end; +add_cubic_bezier_animation(Animation *a, double y_at_start, double y_at_end, double p1x, double p1y, double p2x, double p2y) { + p1x = unit_value(p1x); p2x = unit_value(p2x); + if (p1x == 0 && p1y == 0 && p2x == 1 && p2y == 1) { + init_function(a, y_at_start, y_at_end, identity_easing_curve); + return; + } + BezierParameters *p = calloc(1, sizeof(BezierParameters)); + if (!p) fatal("Out of memory"); + // Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). + p->cx = 3.0 * p1x; + p->bx = 3.0 * (p2x - p1x) - p->cx; + p->ax = 1.0 - p->cx - p->bx; + + p->cy = 3.0 * p1y; + p->by = 3.0 * (p2y - p1y) - p->cy; + p->ay = 1.0 - p->cy - p->by; + + // Calculate gradients used for values outside the unit interval + if (p1x > 0) p->start_gradient = p1y / p1x; + else if (p1y == 0 && p2x > 0) p->start_gradient = p2y / p2x; + else if (p1y == 0 && p2y == 0) p->start_gradient = 1; + else p->start_gradient = 0; + + if (p2x < 1) p->end_gradient = (p2y - 1) / (p2x - 1); + else if (p2y == 1 && p1x < 1) p->end_gradient = (p1y - 1) / (p1x - 1); + else if (p2y == 1 && p1y == 1) p->end_gradient = 1; + else p->end_gradient = 0; + + size_t num_samples = arraysz(p->spline_samples); + double delta = 1. / num_samples; + for (size_t i = 0; i < num_samples; i++) p->spline_samples[i] = sample_curve_x(p, i * delta); + animation_function *f = init_function(a, y_at_start, y_at_end, cubic_bezier_easing_curve); + f->params = p; } void -add_linear_animation(Animation *a, double y_at_start, double y_at_end, size_t count, const double *params, const double *positions) { - animation_function *f = init_function(a, y_at_start, y_at_end, linear_easing_curve, count); +add_linear_animation(Animation *a, double y_at_start, double y_at_end, size_t count, const double *x, const double *y) { const size_t sz = count * sizeof(double); - memcpy((void*)f->params.params, params, sz); memcpy((void*)f->params.positions, positions, sz); + LinearParameters *p = calloc(1, sizeof(LinearParameters) + 2 * sz); + if (!p) fatal("Out of memory"); + p->count = count; + double *px = p->buf, *py = px + count; + memcpy(px, x, sz); memcpy(py, y, sz); + animation_function *f = init_function(a, y_at_start, y_at_end, linear_easing_curve); + f->params = p; } void add_steps_animation(Animation *a, double y_at_start, double y_at_end, size_t count, EasingStep step) { - animation_function *f = init_function(a, y_at_start, y_at_end, step_easing_curve, 0); double jump_size = 1. / count, start_value = 0.; size_t num_of_buckets = count; switch (step) { @@ -143,5 +258,9 @@ add_steps_animation(Animation *a, double y_at_start, double y_at_end, size_t cou start_value = jump_size; break; } - f->params.extra0 = num_of_buckets; f->params.extra1 = jump_size; f->params.extra2 = start_value; + StepsParameters *p = malloc(sizeof(StepsParameters)); + if (!p) fatal("Out of memory"); + p->num_of_buckets = num_of_buckets; p->jump_size = jump_size; p->start_value = start_value; + animation_function *f = init_function(a, y_at_start, y_at_end, step_easing_curve); + f->params = p; } diff --git a/kitty/animation.h b/kitty/animation.h index 9d831fa7e..0f24ca5ac 100644 --- a/kitty/animation.h +++ b/kitty/animation.h @@ -9,15 +9,16 @@ #include #include +#include "monotonic.h" typedef enum { EASING_STEP_START, EASING_STEP_END, EASING_STEP_NONE, EASING_STEP_BOTH } EasingStep; #ifndef ANIMATION_INTERNAL_API typedef struct {int x;} *Animation; #endif Animation* alloc_animation(void); -double apply_easing_curve(const Animation *a, double t /* must be between 0 and 1*/); +double apply_easing_curve(const Animation *a, double t /* must be between 0 and 1*/, monotonic_t duration); bool animation_is_valid(const Animation *a); -void add_cubic_bezier_animation(Animation *a, double y_at_start, double y_at_end, double start, double p1, double p2, double end); -void add_linear_animation(Animation *a, double y_at_start, double y_at_end, size_t count, const double *params, const double *positions); +void add_cubic_bezier_animation(Animation *a, double y_at_start, double y_at_end, double p1_x, double p1_y, double p2_x, double p2_y); +void add_linear_animation(Animation *a, double y_at_start, double y_at_end, size_t count, const double *x, const double *y); void add_steps_animation(Animation *a, double y_at_start, double y_at_end, size_t count, EasingStep step); Animation* free_animation(Animation *a); diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 7b09eda32..a2cd9b2fb 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -679,7 +679,7 @@ collect_cursor_info(CursorRenderInfo *ans, Window *w, monotonic_t now, OSWindow monotonic_t den = OPT(cursor_blink_interval) * 2; monotonic_t time_into_cycle = time_since_start_blink % den; double frac_into_cycle = (double)time_into_cycle / (double)den; - ans->opacity = (float)apply_easing_curve(OPT(animation.cursor), frac_into_cycle); + ans->opacity = (float)apply_easing_curve(OPT(animation.cursor), frac_into_cycle, den); set_maximum_wait(ms_to_monotonic_t(75)); } else { monotonic_t n = time_since_start_blink / OPT(cursor_blink_interval); diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index da70eb6fe..dbdbee664 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -56,6 +56,10 @@ def positive_float(x: ConvertibleToNumbers) -> float: return max(0, float(x)) +def percent(x: str) -> float: + return float(x.rstrip('%')) / 100. + + def to_color(x: str) -> Color: ans = as_color(x, validate=True) if ans is None: # this is only for type-checking diff --git a/kitty/mouse.c b/kitty/mouse.c index 3df364d1f..97ced996b 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -6,13 +6,11 @@ */ #include "state.h" -#include "screen.h" #include "charsets.h" #include #include #include "glfw-wrapper.h" #include "control-codes.h" -#include "monotonic.h" extern PyTypeObject Screen_Type; diff --git a/kitty/options/to-c.h b/kitty/options/to-c.h index ab9d215c9..1fc205551 100644 --- a/kitty/options/to-c.h +++ b/kitty/options/to-c.h @@ -127,25 +127,31 @@ static void add_easing_function(Animation *a, PyObject *e, double y_at_start, double y_at_end) { #define G(name) RAII_PyObject(name, PyObject_GetAttrString(e, #name)) #define D(container, idx) PyFloat_AsDouble(PyTuple_GET_ITEM(container, idx)) +#define EQ(x, val) (PyUnicode_CompareWithASCIIString((x), val) == 0) G(type); - if (PyUnicode_CompareWithASCIIString(type, "cubic-bezier")) { + if (EQ(type, "cubic-bezier")) { G(cubic_bezier_points); add_cubic_bezier_animation(a, y_at_start, y_at_end, D(cubic_bezier_points, 0), D(cubic_bezier_points, 1), D(cubic_bezier_points, 2), D(cubic_bezier_points, 3)); - } else if (PyUnicode_CompareWithASCIIString(type, "linear")) { - G(linear_count); G(linear_params); G(linear_positions); - size_t count = PyLong_AsSize_t(linear_count); - RAII_ALLOC(double, params, malloc(2 * sizeof(double) * count)); - if (params) { - double *positions = params + count; + } else if (EQ(type, "linear")) { + G(linear_x); G(linear_y); + size_t count = PyTuple_GET_SIZE(linear_x); + RAII_ALLOC(double, x, malloc(2 * sizeof(double) * count)); + if (x) { + double *y = x + count; for (size_t i = 0; i < count; i++) { - params[i] = D(linear_params, i); positions[i] = D(linear_positions, i); + x[i] = D(linear_x, i); y[i] = D(y, i); } - add_linear_animation(a, y_at_start, y_at_end, count, params, positions); + add_linear_animation(a, y_at_start, y_at_end, count, x, y); } - } else if (PyUnicode_CompareWithASCIIString(type, "steps")) { + } else if (EQ(type, "steps")) { G(num_steps); G(jump_type); - add_steps_animation(a, y_at_start, y_at_end, PyLong_AsSize_t(num_steps), PyLong_AsLong(jump_type)); + EasingStep jt = EASING_STEP_END; + if (EQ(jump_type, "start")) jt = EASING_STEP_START; + else if (EQ(jump_type, "none")) jt = EASING_STEP_NONE; + else if (EQ(jump_type, "both")) jt = EASING_STEP_BOTH; + add_steps_animation(a, y_at_start, y_at_end, PyLong_AsSize_t(num_steps), jt); } +#undef EQ #undef D #undef G } diff --git a/kitty/options/utils.py b/kitty/options/utils.py index 3bd180b53..eff704af3 100644 --- a/kitty/options/utils.py +++ b/kitty/options/utils.py @@ -6,6 +6,7 @@ import enum import re import sys from collections import defaultdict +from contextlib import suppress from dataclasses import dataclass, fields from functools import lru_cache from typing import ( @@ -34,6 +35,7 @@ from kitty.conf.utils import ( KeyAction, KeyFuncWrapper, currently_parsing, + percent, positive_float, positive_int, python_string, @@ -1393,14 +1395,13 @@ def parse_font_spec(spec: str) -> FontSpec: class EasingFunction(NamedTuple): - type: str = '' + type: Literal['steps', 'linear', 'cubic-bezier', ''] = '' num_steps: int = 0 - jump_type: int = 0 + jump_type: Literal['start', 'end', 'none', 'both'] = 'end' - linear_count: int = 0 - linear_params: Tuple[float, ...] = () - linear_positions: Tuple[float, ...] = () + linear_x: Tuple[float, ...] = () + linear_y: Tuple[float, ...] = () cubic_bezier_points: Tuple[float, ...] = () @@ -1411,13 +1412,125 @@ class EasingFunction(NamedTuple): def __bool__(self) -> bool: return bool(self.type) + @classmethod + def cubic_bezier(cls, params: str) -> 'EasingFunction': + parts = params.replace(',', ' ').split() + if len(parts) != 4: + raise ValueError('cubic-bezier easing function must have four points') + return cls(type='cubic-bezier', cubic_bezier_points=( + unit_float(parts[0]), float(parts[1]), unit_float(parts[2]), float(parts[2]))) + + @classmethod + def linear(cls, params: str) -> 'EasingFunction': + parts = params.split(',') + if len(parts) < 2: + raise ValueError('Must specify at least two points for the linear easing function') + xaxis: List[float] = [] + yaxis: List[float] = [] + + def balance(end: float) -> None: + extra = len(yaxis) - len(xaxis) + if extra <= 0: + return + start = xaxis[-1] if xaxis else 0 + delta = (end - start) / (extra + 1) + if delta <= 0: + raise ValueError(f'Linear easing curve must have strictly increasing points: {params} does not') + for i in range(extra): + xaxis.append((i+1) * delta) + + def add_point(y: float, x: Optional[float] = None) -> None: + if x is None: + yaxis.append(y) + else: + x = unit_float(x) + balance(x) + xaxis.append(x) + yaxis.append(y) + + for r in parts: + points = r.strip().split() + y = unit_float(points[0]) + if len(points) == 1: + add_point(y) + elif len(points) == 2: + add_point(y, percent(points[1])) + elif len(points) == 3: + add_point(y, percent(points[1])) + add_point(y, percent(points[2])) + else: + raise ValueError(f'{r} has too many points for a linear easing curve parameter') + balance(1) + return cls(type='linear', linear_x=tuple(xaxis), linear_y=tuple(yaxis)) + + + @classmethod + def steps(cls, params: str) -> 'EasingFunction': + parts = params.replace(',', ' ').split() + jump_type = 'end' + if len(parts) == 2: + n = int(parts[0]) + jt = parts[1] + try: + jump_type = { + 'jump-start': 'start', 'start': 'start', 'end': 'end', 'jump-end': 'end', 'jump-none': 'none', 'jump-both': 'both' + }[jt.lower()] + except KeyError: + raise KeyError(f'{jt} is not a valid jump type for a linear easing function') + if jump_type == 'none': + n = max(2, n) + else: + n = max(1, n) + else: + n = max(1, int(parts[0])) + return cls(type='steps', jump_type=jump_type, num_steps=n) # type: ignore + def cursor_blink_interval(spec: str) -> Tuple[float, EasingFunction, EasingFunction]: - try: + interval: float = -1 + with suppress(Exception): interval = float(spec) return interval, EasingFunction(), EasingFunction() - except Exception: - return -1, EasingFunction(), EasingFunction() + + m = [EasingFunction(), EasingFunction()] + def parse_func(func_name: str, params: str) -> None: + idx = 1 if m[0] else 0 + if m[idx]: + raise ValueError(f'{spec} specified more than two easing functions') + if func_name == 'cubic-bezier': + m[idx] = EasingFunction.cubic_bezier(params) + elif func_name == 'linear': + m[idx] = EasingFunction.linear(params) + elif func_name == 'steps': + m[idx] = EasingFunction.steps(params) + else: + raise KeyError(f'{func_name} is not a valid easing function') + + for match in re.finditer(r'([-+.0-9a-zA-Z]+)(?:\(([^)]*)\)){0,1}', spec): + func_name, params = match.group(1, 2) + if params: + parse_func(func_name, params) + continue + with suppress(Exception): + interval = float(func_name) + continue + if func_name == 'ease-in-out': + parse_func('cubic-bezier', '0.42, 0, 0.58, 1') + elif func_name == 'linear': + parse_func('cubic-bezier', '0, 0, 1, 1') + elif func_name == 'ease': + parse_func('cubic-bezier', '0.25, 0.1, 0.25, 1') + elif func_name == 'ease-out': + parse_func('cubic-bezier', '0, 0, 0.58, 1') + elif func_name == 'ease-in': + parse_func('cubic-bezier', '0.42, 0, 1, 1') + elif func_name == 'step-start': + parse_func('steps', '1, start') + elif func_name == 'step-end': + parse_func('steps', '1, end') + else: + raise KeyError(f'{func_name} is not a valid easing function') + return interval, m[0], m[1] def deprecated_hide_window_decorations_aliases(key: str, val: str, ans: Dict[str, Any]) -> None: diff --git a/kitty/state.h b/kitty/state.h index 010ccaa44..7df66e696 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -5,6 +5,7 @@ */ #pragma once +#include "data-types.h" #include "animation.h" #include "screen.h" #include "monotonic.h"