~sircmpwn/public-inbox

casa: Implement kinetic scrolling v1 PROPOSED

György Kurucz
György Kurucz: 1
 Implement kinetic scrolling

 5 files changed, 188 insertions(+), 131 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/public-inbox/patches/11452/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH casa] Implement kinetic scrolling Export this patch

György Kurucz
---
 include/casa.h    |   2 +
 include/surface.h |  40 +++++++--
 src/input.c       |  25 +++++-
 src/render.c      |  48 ++---------
 src/surface.c     | 204 +++++++++++++++++++++++++++-------------------
 5 files changed, 188 insertions(+), 131 deletions(-)

diff --git a/include/casa.h b/include/casa.h
index 0b626eb..6c7d496 100644
--- a/include/casa.h
+++ b/include/casa.h
@@ -20,6 +20,8 @@ struct casa_touchpoint {

	int last_x, last_y;
	int32_t last_time;

	double vel_x, vel_y; // moving average of velocity (pixel / msec)
};

struct casa_state {
diff --git a/include/surface.h b/include/surface.h
index 57d0c69..30b4730 100644
--- a/include/surface.h
+++ b/include/surface.h
@@ -28,6 +28,33 @@ enum casa_surface_state {
	CASA_SURFACE_BROWSE,
};

struct casa_drag_integrator {
	uint32_t touchpoints;
	double int_x, int_y;
};

enum casa_animation_kind {
	CASA_ANIMATION_NONE,
	CASA_ANIMATION_LINEAR,
	CASA_ANIMATION_QUADRATIC
};

struct casa_animation {
	int32_t time_start, time_end;
	enum casa_animation_kind kind;
	union {
		struct {
			double s_0, s_1;
		} linear;
		struct {
			// s(t) = s_0 + v_0*t + (1/2)*a*t^2
			double s_0; // pixel
			double v_0; // pixel / msec
			double a; // pixel / msec^2
		} quadratic;
	};
};

struct casa_surface {
	struct casa_state *casa;
	struct wl_list link;
@@ -40,12 +67,14 @@ struct casa_surface {
	struct casa_buffer buffer_pool[3];
	struct wl_callback *frame_callback;
	uint32_t last_frame;
	int32_t anim_due;

	struct wl_list outputs; // casa_surface_output::link

	enum casa_surface_state state;
	enum casa_surface_state next_state;
	double idle_offset;
	double scroll_offset; // (-infinity, idle_offset]
	struct casa_drag_integrator drag_integrator;
	struct casa_animation scroll_offset_anim;
};

struct casa_surface_output {
@@ -58,9 +87,10 @@ struct casa_surface *casa_surface_create(

void casa_surface_schedule_frame(struct casa_surface *surf);

double casa_surface_state_progress(struct casa_surface *surf, int32_t time);
void casa_surface_state_progress(struct casa_surface *surf, int32_t time,
		double *progress, double *y_offs);

void casa_surface_touch_up(struct casa_surface *surf,
		struct casa_touchpoint *tp, int32_t time);
void casa_surface_touch_down(struct casa_surface *surf, int32_t time);
void casa_surface_touch_up(struct casa_surface *surf, int32_t time, double vel_y);

#endif
diff --git a/src/input.c b/src/input.c
index 96b369a..49cbefc 100644
--- a/src/input.c
+++ b/src/input.c
@@ -48,6 +48,11 @@ wl_touch_down(void *data, struct wl_touch *wl_touch,
	tp->last_y = tp->cur_y = tp->init_y =
		(int)(wl_fixed_to_double(y) * surf->scale);
	tp->last_time = tp->cur_time = tp->init_time = time;
	tp->vel_x = tp->vel_y = 0;

	surf->drag_integrator.touchpoints++;

	casa_surface_touch_down(surf, time);
	casa_surface_schedule_frame(surf);
}

@@ -61,10 +66,17 @@ wl_touch_up(void *data, struct wl_touch *wl_touch,
		return;
	}
	struct casa_surface *surf = tp->surface;
	tp->cur_time = time;
	casa_surface_touch_up(surf, tp, time);
	tp->surface = NULL;
	tp->id = -1;

	struct casa_drag_integrator *di = &surf->drag_integrator;
	di->int_x += tp->cur_x - tp->init_x;
	di->int_y += tp->cur_y - tp->init_y;

	if (--di->touchpoints == 0) {
		// last touchpoint gone from this surface
		casa_surface_touch_up(surf, time, tp->vel_y);
	}
}

static void
@@ -82,6 +94,15 @@ wl_touch_motion(void *data, struct wl_touch *wl_touch,
	tp->cur_x = (int)(wl_fixed_to_double(x) * surf->scale);
	tp->cur_y = (int)(wl_fixed_to_double(y) * surf->scale);
	tp->cur_time = time;

	double dt = tp->cur_time - tp->last_time;
	double vel_x = (tp->cur_x - tp->last_x) / dt;
	double vel_y = (tp->cur_y - tp->last_y) / dt;

	// moving average filter
	tp->vel_x = 0.7 * tp->vel_x + 0.3 * vel_x;
	tp->vel_y = 0.7 * tp->vel_y + 0.3 * vel_y;

	casa_surface_schedule_frame(surf);
}

diff --git a/src/render.c b/src/render.c
index 6306917..de1ba3a 100644
--- a/src/render.c
+++ b/src/render.c
@@ -10,11 +10,8 @@

/*
 * TODO:
 * - Animate back to the initial state on incomplete gestures
 * - Don't open/close faster than a certain (short) time
 * - Remember scroll value in BROWSE state
 * - Snap scroll back to bottom if over-scrolled
 * - Implement inertia in scrolling motion
 */
static void
render_icons(struct casa_surface *surface,
@@ -80,36 +77,6 @@ render_icons(struct casa_surface *surface,
	glDisableVertexAttribArray(1);
}

static void
casa_render_idle(struct casa_surface *surface,
		struct casa_buffer *buffer, uint32_t time)
{
	double progress = casa_surface_state_progress(surface, time);
	glClearColor(0.0f, 0.0f, 0.0f, progress * 0.75);
	glClear(GL_COLOR_BUFFER_BIT);

	const int height = 128;
	int y_offs = (int)((1.0 - progress) * (height / 0.5));
	render_icons(surface, buffer, progress, y_offs);
}

static void
casa_render_browse(struct casa_surface *surface,
		struct casa_buffer *buffer, uint32_t time)
{
	double progress = casa_surface_state_progress(surface, time);
	progress = 1.0 - progress;
	glClearColor(0.0f, 0.0f, 0.0f, progress > 1 ? 0.75 : progress * 0.75);
	glClear(GL_COLOR_BUFFER_BIT);

	float height = 128;
	if (progress > 0) {
		height /= 0.5;
	}
	int y_offs = (int)((1.0 - progress) * height);
	render_icons(surface, buffer, progress, y_offs);
}

struct wl_buffer *
casa_render(struct casa_surface *surface, uint32_t time)
{
@@ -120,14 +87,13 @@ casa_render(struct casa_surface *surface, uint32_t time)
			surface->height * surface->scale);
	assert(buffer);

	switch (surface->state) {
	case CASA_SURFACE_IDLE:
		casa_render_idle(surface, buffer, time);
		break;
	case CASA_SURFACE_BROWSE:
		casa_render_browse(surface, buffer, time);
		break;
	}
	double y_offs, progress;
	casa_surface_state_progress(surface, time, &progress, &y_offs);

	glClearColor(0.0f, 0.0f, 0.0f, progress * 0.75);
	glClear(GL_COLOR_BUFFER_BIT);

	render_icons(surface, buffer, progress, y_offs);

	glFinish();

diff --git a/src/surface.c b/src/surface.c
index 8fc02bb..e2adfe1 100644
--- a/src/surface.c
+++ b/src/surface.c
@@ -12,6 +12,24 @@
/* The minimum number of milliseconds an animation will complete in */
const int32_t anim_target_duration = 200;
const double gesture_completion_threshold = 0.25;
const double kinetic_scrolling_friction = 0.01; // pixel / msec^2
const double kinetic_scrolling_max_v = 5.0; // pixel / msec

static void
casa_surface_drag_get(const struct casa_surface *surf, double *x, double *y)
{
	const struct casa_state *state = surf->casa;
	*x = surf->drag_integrator.int_x;
	*y = surf->drag_integrator.int_y;
	for (size_t i = 0;
			i < sizeof(state->touchpoints) / sizeof(state->touchpoints[0]); ++i) {
		const struct casa_touchpoint *point = &state->touchpoints[i];
		if (point->id != -1 && point->surface == surf) {
			*x += point->cur_x - point->init_x;
			*y += point->cur_y - point->init_y;
		}
	}
}

static void
wl_surface_enter(void *data,
@@ -123,6 +141,13 @@ casa_surface_create(struct casa_state *state, struct wl_output *output)
	surf->casa = state;
	wl_list_init(&surf->outputs);

	surf->idle_offset = 256; // TODO: scale this based on something...
	surf->scroll_offset = surf->idle_offset;
	surf->state = CASA_SURFACE_IDLE;
	surf->drag_integrator = (struct casa_drag_integrator){
		.touchpoints = 0, .int_x = 0, .int_y = 0 };
	surf->scroll_offset_anim.time_end = -1;

	surf->surface = wl_compositor_create_surface(state->compositor);
	wl_surface_add_listener(surf->surface, &wl_surface_listener, surf);

@@ -170,114 +195,127 @@ casa_surface_schedule_frame(struct casa_surface *surf)
	wl_surface_commit(surf->surface);
}

static double
casa_surface_state_raw_progress(struct casa_surface *surf, int32_t time,
		struct casa_touchpoint *tp, double *raw)
{
	int height = surf->height * surf->scale;
	int32_t diff_px = 0, diff_ms = 0;
	float extra_px = 0;
	if (tp) {
		diff_px = tp->cur_y - tp->last_y;
		diff_ms = tp->cur_time - tp->last_time;
		if (diff_ms == 0) {
			extra_px = 0;
		} else {
			extra_px = (float)diff_px / (float)diff_ms * (time - tp->cur_time);
		}
	}

	int32_t elapsed = tp ? time - tp->init_time : 0;
	if (elapsed > anim_target_duration) {
		elapsed = anim_target_duration;
	}

	double progress = 0;
	if (surf->anim_due > 0) {
		progress = 1.0 - (surf->anim_due - time) / (float)anim_target_duration;
		elapsed = anim_target_duration;
		if (progress >= 1.0) {
			progress = 1.0;
			surf->anim_due = 0;
			surf->state = surf->next_state;
		}
	}
static void
casa_surface_set_scroll_offset(struct casa_surface *surf, double offs) {
	offs = fmin(offs, surf->idle_offset);
	surf->scroll_offset = offs;
}

	switch (surf->state) {
	case CASA_SURFACE_IDLE:
		if (tp) {
			progress = (tp->init_y - tp->cur_y - extra_px) * 4.0 / height;
		}
		progress = progress > 0 ? progress < 1 ? progress : 1 : 0;
double
casa_animation_val(const struct casa_animation *anim, int32_t time)
{
	double t, val;
	switch (anim->kind) {
	case CASA_ANIMATION_LINEAR:
		t = (time - anim->time_start) / (double)(anim->time_end - anim->time_start);
		val = (1.0 - t) * anim->linear.s_0 + t * anim->linear.s_1;
		break;
	case CASA_SURFACE_BROWSE:
		if (tp) {
			progress = (tp->cur_y - tp->init_y - extra_px) * 4.0 / height;
		}
	case CASA_ANIMATION_QUADRATIC:
		t = time - anim->time_start;
		val = anim->quadratic.s_0 + anim->quadratic.v_0 * t + anim->quadratic.a * t * t / 2;
		break;
	default:
		assert(0);
	}

	if (raw) {
		*raw = progress;
	}

	double max_progress = elapsed / (float)anim_target_duration;
	if (progress > max_progress) {
		progress = max_progress;
	}

	return progress;
	return val;
}

double
casa_surface_state_progress(struct casa_surface *surf, int32_t time)
void
casa_surface_state_progress(struct casa_surface *surf, int32_t time,
		double *progress, double *y_offs)
{
	struct casa_touchpoint *tp = NULL;
	struct casa_state *state = surf->casa;
	for (size_t i = 0;
			i < sizeof(state->touchpoints) / sizeof(state->touchpoints[0]); ++i) {
		if (state->touchpoints[i].surface == surf) {
			tp = &state->touchpoints[i];
			break;
	struct casa_animation *anim = &surf->scroll_offset_anim;

	if (surf->drag_integrator.touchpoints > 0) {
		double off_x, off_y;
		casa_surface_drag_get(surf, &off_x, &off_y);
		*y_offs = fmin(surf->scroll_offset + off_y, surf->idle_offset);
	} else if (anim->kind != CASA_ANIMATION_NONE) {
		if (anim->time_end > time) {
			*y_offs = casa_animation_val(anim, time);
			if (anim->kind == CASA_ANIMATION_QUADRATIC && *y_offs >= 0) {
				// end animation if it would scroll past zero
				casa_surface_set_scroll_offset(surf, 0);
				anim->kind = CASA_ANIMATION_NONE;
				*y_offs = 0;
			}
		} else {
			casa_surface_set_scroll_offset(surf, casa_animation_val(anim, anim->time_end));
			anim->kind = CASA_ANIMATION_NONE;
			*y_offs = surf->scroll_offset;
		}
	} else {
		*y_offs = surf->scroll_offset;
	}

	double progress = casa_surface_state_raw_progress(surf, time, tp, NULL);
	if (progress != 0) {
	*progress = fmin(1.0 - *y_offs / surf->idle_offset, 1.0);

	if (anim->kind != CASA_ANIMATION_NONE) {
		casa_surface_schedule_frame(surf);
	}
	return progress;
}

void
casa_surface_touch_up(struct casa_surface *surf,
		struct casa_touchpoint *tp, int32_t time)
casa_surface_touch_down(struct casa_surface *surf, int32_t time)
{
	struct casa_animation *anim = &surf->scroll_offset_anim;
	if (anim->kind != CASA_ANIMATION_NONE) {
		int32_t t = time > anim->time_end ? anim->time_end : time;
		casa_surface_set_scroll_offset(surf, casa_animation_val(anim, t));
		anim->kind = CASA_ANIMATION_NONE;
	}
}

void
casa_surface_touch_up(struct casa_surface *surf, int32_t time, double vel_y)
{
	double raw;
	double progress = casa_surface_state_raw_progress(surf, time, tp, &raw);
	surf->scroll_offset += surf->drag_integrator.int_y;
	surf->scroll_offset = fmin(surf->scroll_offset, surf->idle_offset);

	// reset integrator
	surf->drag_integrator.int_x = 0;
	surf->drag_integrator.int_y = 0;

	double progress = 1.0 - surf->scroll_offset / surf->idle_offset;

	enum casa_surface_state next_state = surf->state;
	if (raw > gesture_completion_threshold) {
		switch (surf->state) {
		case CASA_SURFACE_IDLE:
	switch (surf->state) {
	case CASA_SURFACE_IDLE:
		if (progress > gesture_completion_threshold) {
			next_state = CASA_SURFACE_BROWSE;
			break;
		case CASA_SURFACE_BROWSE:
		}
		break;
	case CASA_SURFACE_BROWSE:
		if (1 - progress > gesture_completion_threshold) {
			next_state = CASA_SURFACE_IDLE;
			break;
		}
		break;
	}

	if (progress >= 1.0) {
		surf->state = next_state;
	} else if (raw > gesture_completion_threshold) {
		surf->anim_due = time +
			(int32_t)(anim_target_duration * (1.0 - progress));
		surf->next_state = next_state;
	} else {
		/* Animate back to previous state? */
	struct casa_animation *anim = &surf->scroll_offset_anim;
	if (progress > 0.0 && progress < 1.0) {
		anim->kind = CASA_ANIMATION_LINEAR;
		anim->time_start = time;
		anim->linear.s_0 = surf->scroll_offset;
		anim->linear.s_1 = next_state == CASA_SURFACE_BROWSE
				? 0.0 : surf->idle_offset;
		double diff = fabs(anim->linear.s_1 - anim->linear.s_0);
		anim->time_end = anim->time_start +
				(int32_t)(anim_target_duration * (diff / surf->idle_offset));
	} else if (progress >= 1.0) {
		anim->kind = CASA_ANIMATION_QUADRATIC;
		anim->time_start = time;
		anim->quadratic.s_0 = surf->scroll_offset;
		if (fabs(vel_y) > kinetic_scrolling_max_v) {
			anim->quadratic.v_0 = copysign(kinetic_scrolling_max_v, vel_y);
		} else {
			anim->quadratic.v_0 = vel_y;
		}
		anim->quadratic.a = copysign(kinetic_scrolling_friction, -anim->quadratic.v_0);
		anim->time_end = anim->time_start - anim->quadratic.v_0 / anim->quadratic.a;
	}

	surf->state = next_state;

	casa_surface_schedule_frame(surf);
}
-- 
2.27.0
Thanks!

To git@git.sr.ht:~sircmpwn/casa
   01cafa6..3e438f2  master -> master