[libadwaita/wip/exalm/spring: 38/38] s




commit 6a8a3ef3caa325cc917f6cffc98ce203a98fa966
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Fri Jan 8 18:26:26 2021 +0500

    s

 demo/adw-demo-window.c                             |   2 +
 demo/adw-spring-animation-private.h                |  59 +++
 demo/adw-spring-animation.c                        | 320 ++++++++++++++
 demo/adwaita-demo.gresources.xml                   |   8 +
 .../scalable/actions/page-spring-symbolic.svg      |  46 ++
 .../scalable/actions/spring-graph-symbolic.svg     |  55 +++
 .../actions/spring-interactive-symbolic.svg        |  81 ++++
 .../icons/scalable/actions/spring-run-symbolic.svg |  67 +++
 demo/meson.build                                   |  12 +-
 demo/pages/spring/adw-demo-adjustment-row.c        | 145 ++++++
 demo/pages/spring/adw-demo-adjustment-row.h        |  11 +
 demo/pages/spring/adw-demo-adjustment-row.ui       |  61 +++
 demo/pages/spring/adw-demo-page-spring.c           | 193 ++++++++
 demo/pages/spring/adw-demo-page-spring.h           |  11 +
 demo/pages/spring/adw-demo-page-spring.ui          | 154 +++++++
 demo/pages/spring/adw-demo-spring-basic.c          | 487 +++++++++++++++++++++
 demo/pages/spring/adw-demo-spring-basic.h          |  13 +
 demo/pages/spring/adw-demo-spring-basic.ui         | 274 ++++++++++++
 demo/pages/spring/adw-demo-spring-interactive.c    | 292 ++++++++++++
 demo/pages/spring/adw-demo-spring-interactive.h    |  13 +
 demo/pages/spring/adw-demo-spring-interactive.ui   |  42 ++
 demo/pages/spring/adw-demo-spring-preset.c         | 174 ++++++++
 demo/pages/spring/adw-demo-spring-preset.h         |  17 +
 demo/pages/spring/adw-demo-transform-layout.c      | 190 ++++++++
 demo/pages/spring/adw-demo-transform-layout.h      |  18 +
 demo/style.css                                     |  81 ++++
 26 files changed, 2825 insertions(+), 1 deletion(-)
---
diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c
index c75b5eb..696aed8 100644
--- a/demo/adw-demo-window.c
+++ b/demo/adw-demo-window.c
@@ -6,6 +6,7 @@
 
 #include "pages/controls/adw-demo-page-controls.h"
 #include "pages/lists/adw-demo-page-lists.h"
+#include "pages/spring/adw-demo-page-spring.h"
 #include "pages/stub/adw-demo-page-stub.h"
 
 struct _AdwDemoWindow
@@ -95,6 +96,7 @@ adw_demo_window_init (AdwDemoWindow *self)
   ADD_STUB (list, _("Feedback"));
   ADD_STUB (list, _("Status Page"));
   ADD_STUB (list, _("Avatars"));
+  ADD_PAGE (list, _("Spring Animations"), "page-spring-symbolic", ADW_TYPE_DEMO_PAGE_SPRING);
   ADD_STUB (list, _("Menus"));
   ADD_STUB (list, _("Preferences"));
   ADD_STUB (list, _("Keyboard Shortcuts"));
diff --git a/demo/adw-spring-animation-private.h b/demo/adw-spring-animation-private.h
new file mode 100644
index 0000000..09ced3b
--- /dev/null
+++ b/demo/adw-spring-animation-private.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_SPRING_ANIMATION (adw_spring_animation_get_type())
+
+typedef void   (*AdwAnimationValueCallback) (gdouble  value,
+                                             gpointer user_data);
+typedef void   (*AdwAnimationDoneCallback)  (gpointer user_data);
+
+typedef struct _AdwSpringAnimation AdwSpringAnimation;
+
+GType         adw_spring_animation_get_type  (void) G_GNUC_CONST;
+
+AdwSpringAnimation *adw_spring_animation_new       (GtkWidget                 *widget,
+                                                    gdouble                    from,
+                                                    gdouble                    to,
+                                                    gdouble                    velocity,
+                                                    gdouble                    damping,
+                                                    gdouble                    mass,
+                                                    gdouble                    stiffness,
+                                                    gdouble                    epsilon,
+                                                    AdwAnimationValueCallback  value_cb,
+                                                    AdwAnimationDoneCallback   done_cb,
+                                                    gpointer                   user_data);
+
+AdwSpringAnimation *adw_spring_animation_new_with_damping_ratio (GtkWidget                 *widget,
+                                                                 gdouble                    from,
+                                                                 gdouble                    to,
+                                                                 gdouble                    velocity,
+                                                                 gdouble                    damping_ratio,
+                                                                 gdouble                    mass,
+                                                                 gdouble                    stiffness,
+                                                                 gdouble                    epsilon,
+                                                                 AdwAnimationValueCallback  value_cb,
+                                                                 AdwAnimationDoneCallback   done_cb,
+                                                                 gpointer                   user_data);
+
+AdwSpringAnimation *adw_spring_animation_ref       (AdwSpringAnimation *self);
+void                adw_spring_animation_unref     (AdwSpringAnimation *self);
+
+void                adw_spring_animation_start     (AdwSpringAnimation *self);
+void                adw_spring_animation_stop      (AdwSpringAnimation *self);
+
+gdouble             adw_spring_animation_get_value (AdwSpringAnimation *self);
+
+gdouble             adw_spring_animation_get_estimated_duration (AdwSpringAnimation *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (AdwSpringAnimation, adw_spring_animation_unref)
+
+G_END_DECLS
diff --git a/demo/adw-spring-animation.c b/demo/adw-spring-animation.c
new file mode 100644
index 0000000..f0a1fa7
--- /dev/null
+++ b/demo/adw-spring-animation.c
@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ * Copyright (C) 2021 Manuel Genovés <manuel genoves gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "adw-spring-animation-private.h"
+
+#include <adwaita.h>
+#include <math.h>
+
+#define DELTA 0.001
+
+G_DEFINE_BOXED_TYPE (AdwSpringAnimation, adw_spring_animation, adw_spring_animation_ref, 
adw_spring_animation_unref)
+
+struct _AdwSpringAnimation
+{
+  gatomicrefcount ref_count;
+
+  GtkWidget *widget;
+
+  gdouble value;
+
+  gdouble value_from;
+  gdouble value_to;
+
+  gdouble velocity;
+  gdouble damping;
+  gdouble mass;
+  gdouble stiffness;
+  gdouble epsilon;
+
+  gdouble estimated_duration;
+
+  gint64 start_time; /* ms */
+  guint tick_cb_id;
+
+  AdwAnimationValueCallback value_cb;
+  AdwAnimationDoneCallback done_cb;
+  gpointer user_data;
+};
+
+static void
+set_value (AdwSpringAnimation *self,
+           gdouble             value)
+{
+  self->value = value;
+  self->value_cb (value, self->user_data);
+}
+
+/* Based on RBBSpringAnimation from RBBAnimation, MIT license.
+ * https://github.com/robb/RBBAnimation/blob/master/RBBAnimation/RBBSpringAnimation.m
+ */
+static gdouble
+oscillate (AdwSpringAnimation *self,
+           gdouble             t)
+{
+  gdouble b = self->damping;
+  gdouble m = self->mass;
+  gdouble k = self->stiffness;
+  gdouble v0 = self->velocity;
+
+  gdouble beta = b / (2 * m);
+  gdouble omega0 = sqrt (k / m);
+
+  gdouble x0 = -1;
+
+  gdouble envelope = exp (-beta * t);
+
+  /*
+   * Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x)
+   * for the differential equation m*ẍ+b*ẋ+kx = 0
+   */
+
+  if (beta < omega0) { /* Underdamped */
+    gdouble omega1 = sqrt ((omega0 * omega0) - (beta * beta));
+
+    return -x0 + envelope * (x0 * cos (omega1 * t) + ((beta * x0 + v0) / omega1) * sin (omega1 * t));
+  }
+
+  if (beta > omega0) { /* Overdamped */
+    gdouble omega2 = sqrt ((beta * beta) - (omega0 * omega0));
+
+    return -x0 + envelope * (x0 * cosh (omega2 * t) + ((beta * x0 + v0) / omega2) * sinh (omega2 * t));
+  }
+
+  /* Critically damped */
+  return -x0 + envelope * (x0 + (beta * x0 + v0) * t);
+}
+
+static gdouble
+estimate_duration (AdwSpringAnimation *self)
+{
+  gdouble beta = self->damping / (2 * self->mass);
+  gdouble omega0;
+  gdouble x0, y0;
+  gdouble x1, y1;
+  gdouble m;
+
+  if (beta <= 0)
+    return INFINITY;
+
+  omega0 = sqrt (self->stiffness / self->mass);
+
+  /*
+   * As first ansatz for the overdamped solution,
+   * and general estimation for the oscillating ones
+   * we took the value of the envelope when its < epsilon
+   */
+  x0 = -log (self->epsilon) / beta;
+
+  if (beta <= omega0)
+    return x0;
+
+  /*
+   * Since the overdamped solution decays way slower than the envelope
+   * we need to use the value of the oscillation itself.
+   * Newton's root finding method is a good candidate in this particular case:
+   * https://en.wikipedia.org/wiki/Newton%27s_method
+   */
+  y0 = oscillate (self, x0);
+  m = (oscillate (self, x0 + DELTA) - y0) / DELTA;
+
+  x1 = (1 - y0 + m * x0) / m;
+  y1 = oscillate (self, x1);
+
+  while (ABS (1 - y1) > self->epsilon) {
+    x0 = x1;
+    y0 = y1;
+
+    m = (oscillate (self, x0 + DELTA) - y0) / DELTA;
+
+    x1 = (1 - y0 + m * x0) / m;
+    y1 = oscillate (self, x1);
+  }
+
+  return x1;
+}
+
+static inline gdouble
+lerp (gdouble a, gdouble b, gdouble t)
+{
+  return a * (1.0 - t) + b * t;
+}
+
+static gboolean
+tick_cb (GtkWidget          *widget,
+         GdkFrameClock      *frame_clock,
+         AdwSpringAnimation *self)
+{
+  gint64 frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+  gdouble t = (gdouble) (frame_time - self->start_time) / 1000;
+
+  if (t >= self->estimated_duration) {
+    self->tick_cb_id = 0;
+
+    set_value (self, self->value_to);
+
+    g_signal_handlers_disconnect_by_func (self->widget, adw_spring_animation_stop, self);
+
+    self->done_cb (self->user_data);
+
+    return G_SOURCE_REMOVE;
+  }
+
+  set_value (self, lerp (self->value_from, self->value_to, oscillate (self, t)));
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+adw_spring_animation_free (AdwSpringAnimation *self)
+{
+  adw_spring_animation_stop (self);
+
+  g_slice_free (AdwSpringAnimation, self);
+}
+
+AdwSpringAnimation *
+adw_spring_animation_new (GtkWidget                 *widget,
+                          gdouble                    from,
+                          gdouble                    to,
+                          gdouble                    velocity,
+                          gdouble                    damping,
+                          gdouble                    mass,
+                          gdouble                    stiffness,
+                          gdouble                    epsilon,
+                          AdwAnimationValueCallback  value_cb,
+                          AdwAnimationDoneCallback   done_cb,
+                          gpointer                   user_data)
+{
+  AdwSpringAnimation *self;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+  g_return_val_if_fail (damping > 0, NULL);
+  g_return_val_if_fail (mass > 0, NULL);
+  g_return_val_if_fail (stiffness > 0, NULL);
+  g_return_val_if_fail (value_cb != NULL, NULL);
+  g_return_val_if_fail (done_cb != NULL, NULL);
+
+  self = g_slice_new0 (AdwSpringAnimation);
+
+  g_atomic_ref_count_init (&self->ref_count);
+
+  self->widget = widget;
+  self->value_from = from;
+  self->value_to = to;
+  self->velocity = velocity / (to - from);
+  self->damping = damping;
+  self->mass = mass;
+  self->stiffness = stiffness;
+  self->epsilon = epsilon;
+
+  self->value_cb = value_cb;
+  self->done_cb = done_cb;
+  self->user_data = user_data;
+
+  self->value = from;
+
+  self->estimated_duration = estimate_duration (self);
+
+  return self;
+}
+
+AdwSpringAnimation *
+adw_spring_animation_new_with_damping_ratio (GtkWidget                 *widget,
+                                             gdouble                    from,
+                                             gdouble                    to,
+                                             gdouble                    velocity,
+                                             gdouble                    damping_ratio,
+                                             gdouble                    mass,
+                                             gdouble                    stiffness,
+                                             gdouble                    epsilon,
+                                             AdwAnimationValueCallback  value_cb,
+                                             AdwAnimationDoneCallback   done_cb,
+                                             gpointer                   user_data)
+{
+  gdouble critical_damping = 2 * sqrt (mass * stiffness);
+  gdouble damping = damping_ratio * critical_damping;
+
+  return adw_spring_animation_new (widget, from, to, velocity, damping, mass,
+                                   stiffness, epsilon, value_cb, done_cb, user_data);
+}
+
+AdwSpringAnimation *
+adw_spring_animation_ref (AdwSpringAnimation *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  g_atomic_ref_count_inc (&self->ref_count);
+
+  return self;
+}
+
+void
+adw_spring_animation_unref (AdwSpringAnimation *self)
+{
+  g_return_if_fail (self != NULL);
+
+  if (g_atomic_ref_count_dec (&self->ref_count))
+    adw_spring_animation_free (self);
+}
+
+void
+adw_spring_animation_start (AdwSpringAnimation *self)
+{
+  g_return_if_fail (self != NULL);
+
+  if (!adw_get_enable_animations (self->widget) ||
+      !gtk_widget_get_mapped (self->widget) ||
+      ABS (self->value_from - self->value_to) < self->epsilon) {
+    set_value (self, self->value_to);
+
+    self->done_cb (self->user_data);
+
+    return;
+  }
+
+  self->start_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (self->widget)) / 1000;
+
+  if (self->tick_cb_id)
+    return;
+
+  g_signal_connect_swapped (self->widget, "unmap",
+                            G_CALLBACK (adw_spring_animation_stop), self);
+  self->tick_cb_id = gtk_widget_add_tick_callback (self->widget, (GtkTickCallback) tick_cb, self, NULL);
+}
+
+void
+adw_spring_animation_stop (AdwSpringAnimation *self)
+{
+  g_return_if_fail (self != NULL);
+
+  if (!self->tick_cb_id)
+    return;
+
+  gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id);
+  self->tick_cb_id = 0;
+
+  g_signal_handlers_disconnect_by_func (self->widget, adw_spring_animation_stop, self);
+
+  self->done_cb (self->user_data);
+}
+
+gdouble
+adw_spring_animation_get_value (AdwSpringAnimation *self)
+{
+  g_return_val_if_fail (self != NULL, 0.0);
+
+  return self->value;
+}
+
+gdouble
+adw_spring_animation_get_estimated_duration (AdwSpringAnimation *self)
+{
+  g_return_val_if_fail (self != NULL, 0.0);
+
+  return self->estimated_duration;
+}
diff --git a/demo/adwaita-demo.gresources.xml b/demo/adwaita-demo.gresources.xml
index a03c6e5..3605859 100644
--- a/demo/adwaita-demo.gresources.xml
+++ b/demo/adwaita-demo.gresources.xml
@@ -13,10 +13,18 @@
     <file preprocess="xml-stripblanks">icons/scalable/actions/media-playback-start-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/open-link-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/page-lists-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/page-spring-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/spring-interactive-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/spring-graph-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/spring-run-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/star-outline-thick-symbolic.svg</file>
 
     <file preprocess="xml-stripblanks">pages/controls/adw-demo-page-controls.ui</file>
     <file preprocess="xml-stripblanks">pages/lists/adw-demo-page-lists.ui</file>
+    <file preprocess="xml-stripblanks">pages/spring/adw-demo-adjustment-row.ui</file>
+    <file preprocess="xml-stripblanks">pages/spring/adw-demo-page-spring.ui</file>
+    <file preprocess="xml-stripblanks">pages/spring/adw-demo-spring-basic.ui</file>
+    <file preprocess="xml-stripblanks">pages/spring/adw-demo-spring-interactive.ui</file>
     <file preprocess="xml-stripblanks">pages/stub/adw-demo-page-stub.ui</file>
 
     <file preprocess="xml-stripblanks">adw-demo-page.ui</file>
diff --git a/demo/icons/scalable/actions/page-spring-symbolic.svg 
b/demo/icons/scalable/actions/page-spring-symbolic.svg
new file mode 100644
index 0000000..82a72e4
--- /dev/null
+++ b/demo/icons/scalable/actions/page-spring-symbolic.svg
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   width="16px"
+   height="16px"
+   viewBox="0 0 16 16"
+   version="1.1"
+   id="svg13">
+  <metadata
+     id="metadata19">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs17" />
+  <ellipse
+     style="opacity:0.35;fill:#2e3436"
+     id="path8195"
+     cx="3"
+     cy="13"
+     rx="3"
+     ry="3" />
+  <circle
+     style="opacity:0.5;fill:#2e3436"
+     id="ellipse8197"
+     cx="6"
+     cy="10"
+     r="4" />
+  <circle
+     style="fill:#2e3436"
+     id="ellipse8199"
+     cx="10"
+     cy="6"
+     r="5" />
+</svg>
diff --git a/demo/icons/scalable/actions/spring-graph-symbolic.svg 
b/demo/icons/scalable/actions/spring-graph-symbolic.svg
new file mode 100644
index 0000000..0ee440c
--- /dev/null
+++ b/demo/icons/scalable/actions/spring-graph-symbolic.svg
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="24"
+   viewBox="0 0 6.3499999 6.3500002"
+   version="1.1"
+   id="svg59656"
+   sodipodi:docname="spring-graph-symbolic.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     id="namedview842"
+     showgrid="false"
+     inkscape:zoom="13.366944"
+     inkscape:cx="22.135598"
+     inkscape:cy="17.974655"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg59656" />
+  <defs
+     id="defs59650" />
+  <metadata
+     id="metadata59653">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect
 
:none;fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.529167;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1;opacity:1"
+     d="M 3.1757812 0.52929688 C 2.8907242 0.52929688 2.6351672 0.62281489 2.4394531 0.78125 C 2.243739 
0.93968511 2.1061129 1.1533792 2 1.3886719 C 1.7877743 1.8592573 1.6910188 2.4338546 1.5917969 2.9960938 C 
1.4925749 3.5583329 1.3907762 4.1076956 1.2226562 4.4804688 C 1.1385963 4.6668553 1.039789 4.8050324 
0.93164062 4.8925781 C 0.82349227 4.9801239 0.70726879 5.0273437 0.52929688 5.0273438 L 0 5.0273438 L 0 
5.5566406 L 0.52929688 5.5566406 C 0.81435904 5.5566406 1.0699058 5.4631216 1.265625 5.3046875 C 1.4613442 
5.1462534 1.5989614 4.9325588 1.7050781 4.6972656 C 1.9173116 4.2266793 2.0140591 3.6501307 2.1132812 
3.0878906 C 2.2125034 2.5256505 2.3143074 1.9782427 2.4824219 1.6054688 C 2.5664791 1.4190818 2.6633408 
1.2809041 2.7714844 1.1933594 C 2.8796279 1.1058147 2.9978175 1.0585937 3.1757812 1.0585938 C 3.353745 
1.0585938 3.4699818 1.1058148 3.578125 1.1933594 C 3.6862682 1.2809039 3.7850845 1.4190821 3.8691406 
1.6054688 C 4.037253 1.978242 4.1390676 2.5256518 4.2382812 3.08
 78906 C 4.3374949 3.6501295 4.4323192 4.2266808 4.6445312 4.6972656 C 4.7506373 4.932558 4.8882781 5.1462507 
5.0839844 5.3046875 C 5.2796907 5.4631243 5.535263 5.5566406 5.8203125 5.5566406 L 6.3496094 5.5566406 L 
6.3496094 5.0273438 L 5.8203125 5.0273438 C 5.642361 5.0273438 5.5261047 4.9801211 5.4179688 4.8925781 C 
5.3098328 4.8050351 5.2110052 4.6668561 5.1269531 4.4804688 C 4.958849 4.1076941 4.8570264 3.5583341 
4.7578125 2.9960938 C 4.6585986 2.4338534 4.5618331 1.859258 4.3496094 1.3886719 C 4.2434975 1.1533788 
4.1058702 0.93968528 3.9101562 0.78125 C 3.7144423 0.62281472 3.4608383 0.52929688 3.1757812 0.52929688 z "
+     id="path858" />
+</svg>
diff --git a/demo/icons/scalable/actions/spring-interactive-symbolic.svg 
b/demo/icons/scalable/actions/spring-interactive-symbolic.svg
new file mode 100644
index 0000000..0cdd569
--- /dev/null
+++ b/demo/icons/scalable/actions/spring-interactive-symbolic.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb";
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   width="16.000002"
+   viewBox="0 0 16.000002 16.000006"
+   version="1.1"
+   id="svg7384"
+   height="16.000006">
+  <metadata
+     id="metadata90">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <title
+     id="title9167">Gnome Symbolic Icon Theme</title>
+  <defs
+     id="defs7386">
+    <linearGradient
+       osb:paint="solid"
+       id="linearGradient7212">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop7214" />
+    </linearGradient>
+  </defs>
+  <g
+     transform="translate(-261.0002,-177)"
+     style="display:inline"
+     id="layer9" />
+  <g
+     transform="translate(-19.999998,-544)"
+     id="layer1" />
+  <g
+     transform="translate(-19.999998,-544)"
+     style="display:inline"
+     id="layer10" />
+  <g
+     transform="translate(-19.999998,-544)"
+     id="g6387" />
+  <g
+     transform="translate(-19.999998,-544)"
+     id="layer11">
+    <path
+       d="M 28.0219,544 25,547.0148 l 6,10e-6 z"
+       id="path74740"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+    <path
+       d="M 27.03125,546.03125 V 555 557.96875 H 29 V 555 546.03125 Z"
+       id="rect74742"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+    <path
+       d="M 28.0219,560 25,557.00343 l 6,-10e-6 z"
+       id="path74756"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+    <path
+       d="M 20,551.98724 22.99999,555.0057 23,549.01253 Z"
+       id="path74760"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+    <path
+       d="m 22.03125,551 v 2 c 3.98964,0.007 7.97922,0.0322 11.96875,0 v -2 c -3.9883,0.10306 -7.97827,0.014 
-11.96875,0 z"
+       id="rect74762"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+    <path
+       d="M 36,551.98724 33.00001,555.0057 33,549.01253 Z"
+       id="path74764"
+       
style="opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none"
 />
+  </g>
+</svg>
diff --git a/demo/icons/scalable/actions/spring-run-symbolic.svg 
b/demo/icons/scalable/actions/spring-run-symbolic.svg
new file mode 100644
index 0000000..43cfde1
--- /dev/null
+++ b/demo/icons/scalable/actions/spring-run-symbolic.svg
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="24"
+   height="24"
+   viewBox="0 0 6.3499999 6.3500002"
+   version="1.1"
+   id="svg59656"
+   sodipodi:docname="spring-run-symbolic.svg"
+   inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1016"
+     id="namedview7"
+     showgrid="true"
+     inkscape:zoom="12.572452"
+     inkscape:cx="0.391593"
+     inkscape:cy="15.302609"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg59656"
+     inkscape:document-rotation="0">
+    <inkscape:grid
+       type="xygrid"
+       id="grid853" />
+  </sodipodi:namedview>
+  <defs
+     id="defs59650" />
+  <metadata
+     id="metadata59653">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="g3032"
+     inkscape:label="media-playback-start"
+     transform="matrix(0.26458334,0,0,0.26458334,-10.318838,-127.79467)">
+    <path
+       sodipodi:nodetypes="ccccccsccccc"
+       inkscape:connector-curvature="0"
+       id="path3030"
+       d="m 46.00004,488.00319 v 13.99371 h 1.26818 0.131191 c 0.244769,0.001 0.486675,-0.0542 
0.699686,-0.17493 l 9.795594,-5.59748 c 0.434783,-0.24054 0.655955,-0.7325 0.655955,-1.22445 0,-0.49194 
-0.221172,-0.98391 -0.655955,-1.22445 l -9.795594,-5.59748 c -0.213011,-0.12076 -0.454917,-0.17676 
-0.699686,-0.17492 H 47.26822 Z"
+       
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Bitstream
 Vera Sans';-inkscape-font-specification:'Bitstream Vera 
Sans';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-anchor:start;overflow:visible;fill:#241f31;fill-opacity:1;stroke:none;stroke-width:2;enable-background:accumulate"
 />
+  </g>
+</svg>
diff --git a/demo/meson.build b/demo/meson.build
index 679cdff..8a64476 100644
--- a/demo/meson.build
+++ b/demo/meson.build
@@ -12,19 +12,29 @@ adwaita_demo_sources = [
 
   'pages/controls/adw-demo-page-controls.c',
   'pages/lists/adw-demo-page-lists.c',
+  'pages/spring/adw-demo-adjustment-row.c',
+  'pages/spring/adw-demo-page-spring.c',
+  'pages/spring/adw-demo-spring-basic.c',
+  'pages/spring/adw-demo-spring-interactive.c',
+  'pages/spring/adw-demo-spring-preset.c',
+  'pages/spring/adw-demo-transform-layout.c',
   'pages/stub/adw-demo-page-stub.c',
 
   'adwaita-demo.c',
   'adw-demo-page.c',
   'adw-demo-page-info.c',
   'adw-demo-window.c',
+  'adw-spring-animation.c',
 
   libadwaita_generated_headers,
 ]
 
 adwaita_demo = executable('adwaita-@0@-demo'.format(apiversion),
   adwaita_demo_sources,
-  dependencies: libadwaita_dep,
+  dependencies: [
+    libadwaita_dep,
+    cc.find_library('m', required: false),
+  ],
   gui_app: true,
   install: true,
 )
diff --git a/demo/pages/spring/adw-demo-adjustment-row.c b/demo/pages/spring/adw-demo-adjustment-row.c
new file mode 100644
index 0000000..4a9ccdc
--- /dev/null
+++ b/demo/pages/spring/adw-demo-adjustment-row.c
@@ -0,0 +1,145 @@
+#include "adw-demo-adjustment-row.h"
+
+#include <glib/gi18n.h>
+
+struct _AdwDemoAdjustmentRow
+{
+  AdwBin parent_instance;
+
+  gchar *title;
+  guint digits;
+  GtkAdjustment *adjustment;
+};
+
+G_DEFINE_TYPE (AdwDemoAdjustmentRow, adw_demo_adjustment_row, ADW_TYPE_BIN);
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_DIGITS,
+  PROP_ADJUSTMENT,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static inline void
+set_string (gchar       **dest,
+            const gchar  *source)
+{
+  if (*dest)
+    g_free (*dest);
+
+  *dest = g_strdup (source);
+}
+
+static void
+adw_demo_adjustment_row_dispose (GObject *object)
+{
+  AdwDemoAdjustmentRow *self = ADW_DEMO_ADJUSTMENT_ROW (object);
+
+  g_clear_object (&self->adjustment);
+
+  G_OBJECT_CLASS (adw_demo_adjustment_row_parent_class)->dispose (object);
+}
+
+static void
+adw_demo_adjustment_row_finalize (GObject *object)
+{
+  AdwDemoAdjustmentRow *self = ADW_DEMO_ADJUSTMENT_ROW (object);
+
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (adw_demo_adjustment_row_parent_class)->finalize (object);
+}
+
+static void
+adw_demo_adjustment_row_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  AdwDemoAdjustmentRow *self = ADW_DEMO_ADJUSTMENT_ROW (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    g_value_set_string (value, self->title);
+    break;
+  case PROP_DIGITS:
+    g_value_set_uint (value, self->digits);
+    break;
+  case PROP_ADJUSTMENT:
+    g_value_set_object (value, self->adjustment);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_adjustment_row_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  AdwDemoAdjustmentRow *self = ADW_DEMO_ADJUSTMENT_ROW (object);
+
+  switch (prop_id) {
+  case PROP_TITLE:
+    set_string (&self->title, g_value_get_string (value));
+    break;
+  case PROP_DIGITS:
+    self->digits = g_value_get_uint (value);
+    break;
+  case PROP_ADJUSTMENT:
+    g_set_object (&self->adjustment, g_value_get_object (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_adjustment_row_class_init (AdwDemoAdjustmentRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_demo_adjustment_row_dispose;
+  object_class->finalize = adw_demo_adjustment_row_finalize;
+  object_class->get_property = adw_demo_adjustment_row_get_property;
+  object_class->set_property = adw_demo_adjustment_row_set_property;
+
+  props[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         _("Title"),
+                         _("Title"),
+                         NULL,
+                         G_PARAM_READWRITE);
+
+  props[PROP_DIGITS] =
+    g_param_spec_uint ("digits",
+                       _("Digits"),
+                       _("Digits"),
+                       0, G_MAXUINT, 0,
+                       G_PARAM_READWRITE);
+
+  props[PROP_ADJUSTMENT] =
+    g_param_spec_object ("adjustment",
+                         _("Adjustment"),
+                         _("Adjustment"),
+                         GTK_TYPE_ADJUSTMENT,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Adwaita/Demo/pages/spring/adw-demo-adjustment-row.ui");
+}
+
+static void
+adw_demo_adjustment_row_init (AdwDemoAdjustmentRow *self)
+{
+  self->digits = 0;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/demo/pages/spring/adw-demo-adjustment-row.h b/demo/pages/spring/adw-demo-adjustment-row.h
new file mode 100644
index 0000000..36f2fe9
--- /dev/null
+++ b/demo/pages/spring/adw-demo-adjustment-row.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_ADJUSTMENT_ROW (adw_demo_adjustment_row_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoAdjustmentRow, adw_demo_adjustment_row, ADW, DEMO_ADJUSTMENT_ROW, AdwBin)
+
+G_END_DECLS
diff --git a/demo/pages/spring/adw-demo-adjustment-row.ui b/demo/pages/spring/adw-demo-adjustment-row.ui
new file mode 100644
index 0000000..1027405
--- /dev/null
+++ b/demo/pages/spring/adw-demo-adjustment-row.ui
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <template class="AdwDemoAdjustmentRow" parent="AdwBin">
+    <property name="hexpand-set">True</property>
+    <child>
+      <object class="GtkGrid">
+        <property name="column-spacing">12</property>
+        <property name="margin-top">12</property>
+        <property name="margin-bottom">6</property>
+        <style>
+          <class name="header"/>
+        </style>
+        <child>
+          <object class="GtkLabel">
+            <property name="margin-start">12</property>
+            <property name="hexpand">True</property>
+            <property name="halign">start</property>
+            <property name="ellipsize">end</property>
+            <property name="xalign">0</property>
+            <binding name="label">
+              <lookup name="title">AdwDemoAdjustmentRow</lookup>
+            </binding>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSpinButton">
+            <property name="valign">center</property>
+            <property name="margin-end">12</property>
+            <binding name="adjustment">
+              <lookup name="adjustment">AdwDemoAdjustmentRow</lookup>
+            </binding>
+            <binding name="digits">
+              <lookup name="digits">AdwDemoAdjustmentRow</lookup>
+            </binding>
+            <layout>
+              <property name="column">1</property>
+            </layout>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScale">
+            <property name="valign">center</property>
+            <binding name="adjustment">
+              <lookup name="adjustment">AdwDemoAdjustmentRow</lookup>
+            </binding>
+            <layout>
+              <property name="column">0</property>
+              <property name="row">1</property>
+              <property name="column-span">2</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demo/pages/spring/adw-demo-page-spring.c b/demo/pages/spring/adw-demo-page-spring.c
new file mode 100644
index 0000000..436c101
--- /dev/null
+++ b/demo/pages/spring/adw-demo-page-spring.c
@@ -0,0 +1,193 @@
+#include "adw-demo-page-spring.h"
+
+#include <glib/gi18n.h>
+
+#include "adw-demo-adjustment-row.h"
+#include "adw-demo-spring-basic.h"
+#include "adw-demo-spring-interactive.h"
+#include "adw-demo-spring-preset.h"
+#include "adw-spring-animation-private.h"
+
+struct _AdwDemoPageSpring
+{
+  AdwDemoPage parent_instance;
+
+  AdwDemoSpringBasic *basic_view;
+  AdwDemoSpringInteractive *interactive_view;
+
+  GListStore *presets;
+  AdwComboRow *presets_row;
+
+  gdouble damping;
+  gdouble mass;
+  gdouble stiffness;
+  gdouble precision;
+};
+
+G_DEFINE_TYPE (AdwDemoPageSpring, adw_demo_page_spring, ADW_TYPE_DEMO_PAGE)
+
+enum {
+  PROP_0,
+  PROP_DAMPING,
+  PROP_MASS,
+  PROP_STIFFNESS,
+  PROP_PRECISION,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+reset (AdwDemoPageSpring *self)
+{
+  adw_demo_spring_basic_reset (self->basic_view);
+  adw_demo_spring_interactive_reset (self->interactive_view);
+}
+
+static void
+apply_preset (AdwDemoPageSpring   *self,
+              AdwDemoSpringPreset *preset)
+{
+  gdouble damping, mass, stiffness, precision;
+
+  g_object_get (preset,
+                "damping", &damping,
+                "mass", &mass,
+                "stiffness", &stiffness,
+                "precision", &precision,
+                NULL);
+
+  g_object_set (self,
+                "damping", damping,
+                "mass", mass,
+                "stiffness", stiffness,
+                "precision", precision,
+                NULL);
+}
+
+static void
+preset_cb (AdwDemoPageSpring *self)
+{
+  apply_preset (self, adw_combo_row_get_selected_item (self->presets_row));
+}
+
+static void
+adw_demo_page_spring_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  AdwDemoPageSpring *self = ADW_DEMO_PAGE_SPRING (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    g_value_set_double (value, self->damping);
+    break;
+  case PROP_MASS:
+    g_value_set_double (value, self->mass);
+    break;
+  case PROP_STIFFNESS:
+    g_value_set_double (value, self->stiffness);
+    break;
+  case PROP_PRECISION:
+    g_value_set_double (value, self->precision);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_page_spring_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  AdwDemoPageSpring *self = ADW_DEMO_PAGE_SPRING (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    self->damping = g_value_get_double (value);
+    break;
+  case PROP_MASS:
+    self->mass = g_value_get_double (value);
+    break;
+  case PROP_STIFFNESS:
+    self->stiffness = g_value_get_double (value);
+    break;
+  case PROP_PRECISION:
+    self->precision = g_value_get_double (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_page_spring_class_init (AdwDemoPageSpringClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = adw_demo_page_spring_get_property;
+  object_class->set_property = adw_demo_page_spring_set_property;
+
+  props[PROP_DAMPING] =
+    g_param_spec_double ("damping",
+                         _("Damping"),
+                         _("Damping"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_MASS] =
+    g_param_spec_double ("mass",
+                         _("Mass"),
+                         _("Mass"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_STIFFNESS] =
+    g_param_spec_double ("stiffness",
+                         _("Stiffness"),
+                         _("Stiffness"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_PRECISION] =
+    g_param_spec_double ("precision",
+                         _("Precision"),
+                         _("Precision"),
+                         0, 1, 0,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Adwaita/Demo/pages/spring/adw-demo-page-spring.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoPageSpring, basic_view);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoPageSpring, interactive_view);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoPageSpring, presets);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoPageSpring, presets_row);
+  gtk_widget_class_bind_template_callback (widget_class, reset);
+  gtk_widget_class_bind_template_callback (widget_class, preset_cb);
+}
+
+static void
+adw_demo_page_spring_init (AdwDemoPageSpring *self)
+{
+  g_type_ensure (ADW_TYPE_DEMO_ADJUSTMENT_ROW);
+  g_type_ensure (ADW_TYPE_DEMO_SPRING_BASIC);
+  g_type_ensure (ADW_TYPE_DEMO_SPRING_INTERACTIVE);
+  g_type_ensure (ADW_TYPE_DEMO_SPRING_PRESET);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (10,  1, 100, 0.001, _("Default (Core 
Animation)")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (26,  1, 170, 0.001, _("Default 
(react-spring)")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (14,  1, 120, 0.001, _("Gentle")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (12,  1, 180, 0.001, _("Wobbly")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (20,  1, 210, 0.001, _("Stiff")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (60,  1, 280, 0.001, _("Slow")));
+  g_list_store_append (self->presets, adw_demo_spring_preset_new (120, 1, 280, 0.001, _("Molasses")));
+
+  preset_cb (self);
+}
diff --git a/demo/pages/spring/adw-demo-page-spring.h b/demo/pages/spring/adw-demo-page-spring.h
new file mode 100644
index 0000000..28845d2
--- /dev/null
+++ b/demo/pages/spring/adw-demo-page-spring.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "adw-demo-page.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_PAGE_SPRING (adw_demo_page_spring_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoPageSpring, adw_demo_page_spring, ADW, DEMO_PAGE_SPRING, AdwDemoPage)
+
+G_END_DECLS
diff --git a/demo/pages/spring/adw-demo-page-spring.ui b/demo/pages/spring/adw-demo-page-spring.ui
new file mode 100644
index 0000000..03b77c7
--- /dev/null
+++ b/demo/pages/spring/adw-demo-page-spring.ui
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <template class="AdwDemoPageSpring" parent="AdwDemoPage">
+    <property name="title" translatable="yes">Spring Animations</property>
+    <property name="child">
+      <object class="AdwPreferencesPage">
+        <property name="vexpand">True</property>
+        <child>
+          <object class="AdwPreferencesGroup">
+            <child>
+              <object class="GtkStackSwitcher">
+                <property name="stack">stack</property>
+                <property name="halign">center</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="AdwPreferencesGroup">
+            <child>
+              <object class="GtkStack" id="stack">
+                <property name="transition-type">crossfade</property>
+                <style>
+                  <class name="content"/>
+                </style>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="title" translatable="yes">Basic</property>
+                    <property name="child">
+                      <object class="AdwDemoSpringBasic" id="basic_view">
+                        <property name="damping"   bind-source="AdwDemoPageSpring" bind-property="damping"   
bind-flags="sync-create"/>
+                        <property name="mass"      bind-source="AdwDemoPageSpring" bind-property="mass"      
bind-flags="sync-create"/>
+                        <property name="stiffness" bind-source="AdwDemoPageSpring" bind-property="stiffness" 
bind-flags="sync-create"/>
+                        <property name="precision" bind-source="AdwDemoPageSpring" bind-property="precision" 
bind-flags="sync-create"/>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="title" translatable="yes">Interactive</property>
+                    <property name="child">
+                      <object class="AdwDemoSpringInteractive" id="interactive_view">
+                        <property name="damping"   bind-source="AdwDemoPageSpring" bind-property="damping"   
bind-flags="sync-create"/>
+                        <property name="mass"      bind-source="AdwDemoPageSpring" bind-property="mass"      
bind-flags="sync-create"/>
+                        <property name="stiffness" bind-source="AdwDemoPageSpring" bind-property="stiffness" 
bind-flags="sync-create"/>
+                        <property name="precision" bind-source="AdwDemoPageSpring" bind-property="precision" 
bind-flags="sync-create"/>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="AdwPreferencesGroup">
+            <child>
+              <object class="AdwComboRow" id="presets_row">
+                <property name="title" translatable="yes">Preset</property>
+                <signal name="notify::selected-item" handler="preset_cb" swapped="true"/>
+                <property name="model">
+                  <object class="GListStore" id="presets"/>
+                </property>
+                <property name="expression">
+                  <lookup name="name" type="AdwDemoSpringPreset"/>
+                </property>`
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="AdwPreferencesGroup">
+            <child>
+              <object class="GtkFlowBox">
+                <property name="selection-mode">none</property>
+                <property name="min-children-per-line">1</property>
+                <property name="max-children-per-line">2</property>
+                <property name="row-spacing">1</property>
+                <property name="column-spacing">1</property>
+                <property name="overflow">hidden</property>
+                <style>
+                  <class name="content"/>
+                </style>
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="title" translatable="yes">Damping</property>
+                    <property name="adjustment">damping</property>
+                    <property name="digits">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="title" translatable="yes">Mass</property>
+                    <property name="adjustment">mass</property>
+                    <property name="digits">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="title" translatable="yes">Stiffness</property>
+                    <property name="adjustment">stiffness</property>
+                    <property name="digits">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="title" translatable="yes">Precision</property>
+                    <property name="adjustment">precision</property>
+                    <property name="digits">4</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+  <object class="GtkAdjustment" id="damping">
+    <property name="lower">0</property>
+    <property name="upper">200</property>
+    <property name="value" bind-source="AdwDemoPageSpring" bind-property="damping" 
bind-flags="bidirectional"/>
+    <property name="step-increment">1</property>
+    <property name="page-increment">10</property>
+    <signal name="value-changed" handler="reset" swapped="true"/>
+  </object>
+  <object class="GtkAdjustment" id="mass">
+    <property name="lower">0</property>
+    <property name="upper">10</property>
+    <property name="value" bind-source="AdwDemoPageSpring" bind-property="mass" bind-flags="bidirectional"/>
+    <property name="step-increment">1</property>
+    <property name="page-increment">10</property>
+    <signal name="value-changed" handler="reset" swapped="true"/>
+  </object>
+  <object class="GtkAdjustment" id="stiffness">
+    <property name="lower">0</property>
+    <property name="upper">400</property>
+    <property name="value" bind-source="AdwDemoPageSpring" bind-property="stiffness" 
bind-flags="bidirectional"/>
+    <property name="step-increment">1</property>
+    <property name="page-increment">10</property>
+    <signal name="value-changed" handler="reset" swapped="true"/>
+  </object>
+  <object class="GtkAdjustment" id="precision">
+    <property name="lower">0.0001</property>
+    <property name="upper">0.01</property>
+    <property name="value" bind-source="AdwDemoPageSpring" bind-property="precision" 
bind-flags="bidirectional"/>
+    <property name="step-increment">0.001</property>
+    <property name="page-increment">0.1</property>
+    <signal name="value-changed" handler="reset" swapped="true"/>
+  </object>
+</interface>
diff --git a/demo/pages/spring/adw-demo-spring-basic.c b/demo/pages/spring/adw-demo-spring-basic.c
new file mode 100644
index 0000000..6f983f4
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-basic.c
@@ -0,0 +1,487 @@
+#include "adw-demo-spring-basic.h"
+
+#include <glib/gi18n.h>
+
+#include "adw-demo-transform-layout.h"
+#include "adw-spring-animation-private.h"
+
+#include <math.h>
+
+#define GRAPH_PADDING 24
+
+typedef struct {
+  gint64 time;
+  gdouble value;
+} GraphPoint;
+
+struct _AdwDemoSpringBasic
+{
+  AdwBin parent_instance;
+
+  gdouble damping;
+  gdouble mass;
+  gdouble stiffness;
+  gdouble precision;
+
+  AdwDemoTransformLayout *scale_layout;
+  AdwDemoTransformLayout *htranslate_layout;
+  AdwDemoTransformLayout *rotate_layout;
+  AdwDemoTransformLayout *vtranslate_layout;
+  GtkAdjustment *velocity;
+  GtkToggleButton *info_btn;
+  GtkStack *stack;
+  GtkDrawingArea *darea;
+  GtkWidget *label_box;
+  GtkLabel *duration_label;
+  GtkLabel *min_label;
+  GtkLabel *max_label;
+
+  AdwSpringAnimation *animation;
+  gboolean invert;
+
+  AdwSpringAnimation *graph_animation;
+  GArray *points;
+  gint64 start_time;
+  gint64 duration;
+  gdouble min;
+  gdouble max;
+};
+
+G_DEFINE_TYPE (AdwDemoSpringBasic, adw_demo_spring_basic, ADW_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_DAMPING,
+  PROP_MASS,
+  PROP_STIFFNESS,
+  PROP_PRECISION,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static inline gfloat
+lerp (gfloat a, gfloat b, gfloat t)
+{
+  return a * (1.0 - t) + b * t;
+}
+
+static void
+set_value (AdwDemoSpringBasic *self,
+           gdouble             value)
+{
+  gfloat x = lerp (-30, 30, value);
+  gfloat y = lerp (30, -30, value);
+  gfloat s = MAX (0, lerp (3, 1, value));
+  gfloat r = lerp (0, 90, value);
+
+  adw_demo_transform_layout_take_transform (self->htranslate_layout,
+                                            gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (x, 0)));
+  adw_demo_transform_layout_take_transform (self->vtranslate_layout,
+                                            gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, y)));
+  adw_demo_transform_layout_take_transform (self->scale_layout,
+                                            gsk_transform_scale (NULL, s, s));
+  adw_demo_transform_layout_take_transform (self->rotate_layout,
+                                            gsk_transform_rotate (NULL, r));
+}
+
+static void
+set_min (AdwDemoSpringBasic *self,
+         gdouble             min)
+{
+  g_autofree gchar *label = g_strdup_printf (_("Min: %.2lf"), min);
+
+  self->min = min;
+
+  gtk_label_set_label (self->min_label, label);
+}
+
+static void
+set_max (AdwDemoSpringBasic *self,
+         gdouble             max)
+{
+  g_autofree gchar *label = g_strdup_printf (_("Max: %.2lf"), max);
+
+  self->max = max;
+
+  gtk_label_set_label (self->max_label, label);
+}
+
+static void
+add_plot_point (AdwDemoSpringBasic *self,
+                gint64              time,
+                gdouble             value)
+{
+  GraphPoint point = { time - self->start_time, value };
+
+  g_array_append_val (self->points, point);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self->darea));
+
+  if (value < self->min)
+    set_min (self, value);
+
+  if (value > self->max)
+    set_max (self, value);
+}
+
+static void
+active_changed_cb (AdwDemoSpringBasic *self)
+{
+  const gchar *name = gtk_toggle_button_get_active (self->info_btn) ? "info" : "basic";
+
+  gtk_stack_set_visible_child_name (self->stack, name);
+}
+
+static void
+graph_value_cb (gdouble             value,
+                AdwDemoSpringBasic *self)
+{
+  gint64 frame_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (GTK_WIDGET (self)));
+
+  add_plot_point (self, frame_time, value);
+}
+
+static void
+graph_done_cb (AdwDemoSpringBasic *self)
+{
+  g_clear_pointer (&self->graph_animation, adw_spring_animation_unref);
+}
+
+static void
+run_graph (AdwDemoSpringBasic *self)
+{
+  gdouble velocity = gtk_adjustment_get_value (self->velocity);
+  gdouble duration;
+
+  if (self->graph_animation)
+    adw_spring_animation_stop (self->graph_animation);
+
+  g_clear_pointer (&self->points, g_array_unref);
+  self->points = g_array_new (FALSE, FALSE, sizeof (GraphPoint));
+
+  self->start_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (GTK_WIDGET (self)));
+
+  set_min (self, 0);
+  set_max (self, 1);
+  add_plot_point (self, self->start_time, 0);
+
+  self->graph_animation = adw_spring_animation_new (GTK_WIDGET (self),
+                                                    0,
+                                                    1,
+                                                    velocity,
+                                                    self->damping,
+                                                    self->mass,
+                                                    self->stiffness,
+                                                    self->precision,
+                                                    (AdwAnimationValueCallback) graph_value_cb,
+                                                    (AdwAnimationDoneCallback) graph_done_cb,
+                                                    self);
+
+  duration = adw_spring_animation_get_estimated_duration (self->graph_animation);
+
+  if (isinf (duration)) {
+    self->duration = -1;
+
+    gtk_label_set_label (self->duration_label, _("Duration: ∞"));
+  } else {
+    g_autofree gchar *label = g_strdup_printf (_("Duration: %.0lfms"), duration * 1000);
+
+    self->duration = (gint64) (duration * 1000000);
+    gtk_label_set_label (self->duration_label, label);
+  }
+
+  adw_spring_animation_start (self->graph_animation);
+
+  gtk_widget_set_opacity (self->label_box, 1);
+}
+
+static void
+basic_value_cb (gdouble             value,
+                AdwDemoSpringBasic *self)
+{
+  set_value (self, value);
+}
+
+static void
+basic_done_cb (AdwDemoSpringBasic *self)
+{
+  g_clear_pointer (&self->animation, adw_spring_animation_unref);
+}
+
+static void
+run_basic (AdwDemoSpringBasic *self)
+{
+  gdouble value = self->invert ? 1 : 0;
+  gdouble velocity = gtk_adjustment_get_value (self->velocity);
+
+  if (self->animation) {
+    value = adw_spring_animation_get_value (self->animation);
+    adw_spring_animation_stop (self->animation);
+  }
+
+  self->animation = adw_spring_animation_new (GTK_WIDGET (self),
+                                              value,
+                                              self->invert ? 0 : 1,
+                                              velocity,
+                                              self->damping,
+                                              self->mass,
+                                              self->stiffness,
+                                              self->precision,
+                                              (AdwAnimationValueCallback) basic_value_cb,
+                                              (AdwAnimationDoneCallback) basic_done_cb,
+                                              self);
+
+  adw_spring_animation_start (self->animation);
+
+  self->invert = !self->invert;
+}
+
+static void
+run_cb (AdwDemoSpringBasic *self)
+{
+  if (gtk_toggle_button_get_active (self->info_btn))
+    run_graph (self);
+  else
+    run_basic (self);
+}
+
+static inline void
+set_color_from_css (AdwDemoSpringBasic *self,
+                    cairo_t            *cr,
+                    const gchar        *name,
+                    gdouble             alpha_multiplier)
+{
+  GdkRGBA rgba = { 0, 0, 0, 1 };
+
+  gtk_style_context_lookup_color (gtk_widget_get_style_context (GTK_WIDGET (self->darea)), name, &rgba);
+
+  cairo_set_source_rgba (cr, rgba.red, rgba.green, rgba.blue, rgba.alpha * alpha_multiplier);
+}
+
+static inline double
+transform_y (AdwDemoSpringBasic *self,
+             gdouble             height,
+             gdouble             y)
+{
+  gdouble bottom_padding = gtk_widget_get_allocated_height (self->label_box);
+
+  return height - (bottom_padding + (y - self->min) * (height - GRAPH_PADDING - bottom_padding) / (self->max 
- self->min));
+}
+
+static void
+draw_cb (GtkDrawingArea     *darea,
+         cairo_t            *cr,
+         gint                width,
+         gint                height,
+         AdwDemoSpringBasic *self)
+{
+  gdouble x = 0;
+  gdouble y1 = transform_y (self, height, 0);
+  gdouble y2 = transform_y (self, height, 1);
+  gdouble dashes[2] = { 4, 2 };
+  gint64 d;
+  guint i;
+  cairo_path_t *path;
+
+  cairo_save (cr);
+
+  cairo_set_line_width (cr, 1);
+  cairo_set_dash (cr, dashes, 2, 0);
+  cairo_translate(cr, 0, 0.5);
+
+  set_color_from_css (self, cr, "borders", 1);
+
+  cairo_move_to (cr, 0, y1);
+  cairo_line_to (cr, width, y1);
+
+  cairo_move_to (cr, 0, y2);
+  cairo_line_to (cr, width, y2);
+
+  cairo_stroke (cr);
+
+  cairo_restore (cr);
+
+  if (!self->points)
+    return;
+
+  cairo_new_path (cr);
+
+  d = self->duration < 0 ? 10000000 : self->duration;
+
+  for (i = 0; i < self->points->len; i++) {
+    GraphPoint *point = &g_array_index (self->points, GraphPoint, i);
+
+    x = (double) point->time * width / d;
+
+    cairo_line_to (cr, x, transform_y (self, height, point->value));
+  }
+
+  path = cairo_copy_path (cr);
+
+  set_color_from_css (self, cr, "yellow_1", 0.5);
+
+  cairo_line_to (cr, x, height);
+  cairo_line_to (cr, -1, height);
+  cairo_close_path (cr);
+  cairo_fill (cr);
+
+  cairo_append_path (cr, path);
+
+  cairo_set_line_width (cr, 2);
+  set_color_from_css (self, cr, "yellow_5", 1);
+  cairo_stroke (cr);
+
+  cairo_path_destroy (path);
+}
+
+static void
+adw_demo_spring_basic_finalize (GObject *object)
+{
+  AdwDemoSpringBasic *self = ADW_DEMO_SPRING_BASIC (object);
+
+  g_clear_pointer (&self->points, g_array_unref);
+
+  G_OBJECT_CLASS (adw_demo_spring_basic_parent_class)->finalize (object);
+}
+
+static void
+adw_demo_spring_basic_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  AdwDemoSpringBasic *self = ADW_DEMO_SPRING_BASIC (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    g_value_set_double (value, self->damping);
+    break;
+  case PROP_MASS:
+    g_value_set_double (value, self->mass);
+    break;
+  case PROP_STIFFNESS:
+    g_value_set_double (value, self->stiffness);
+    break;
+  case PROP_PRECISION:
+    g_value_set_double (value, self->precision);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_basic_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  AdwDemoSpringBasic *self = ADW_DEMO_SPRING_BASIC (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    self->damping = g_value_get_double (value);
+    break;
+  case PROP_MASS:
+    self->mass = g_value_get_double (value);
+    break;
+  case PROP_STIFFNESS:
+    self->stiffness = g_value_get_double (value);
+    break;
+  case PROP_PRECISION:
+    self->precision = g_value_get_double (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_basic_class_init (AdwDemoSpringBasicClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = adw_demo_spring_basic_finalize;
+  object_class->get_property = adw_demo_spring_basic_get_property;
+  object_class->set_property = adw_demo_spring_basic_set_property;
+
+  props[PROP_DAMPING] =
+    g_param_spec_double ("damping",
+                         _("Damping"),
+                         _("Damping"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_MASS] =
+    g_param_spec_double ("mass",
+                         _("Mass"),
+                         _("Mass"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_STIFFNESS] =
+    g_param_spec_double ("stiffness",
+                         _("Stiffness"),
+                         _("Stiffness"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_PRECISION] =
+    g_param_spec_double ("precision",
+                         _("Precision"),
+                         _("Precision"),
+                         0, 1, 0,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Adwaita/Demo/pages/spring/adw-demo-spring-basic.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, scale_layout);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, htranslate_layout);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, rotate_layout);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, vtranslate_layout);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, velocity);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, info_btn);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, stack);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, darea);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, label_box);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, duration_label);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, min_label);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringBasic, max_label);
+  gtk_widget_class_bind_template_callback (widget_class, active_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, run_cb);
+  gtk_widget_class_bind_template_callback (widget_class, adw_demo_spring_basic_reset);
+}
+
+static void
+adw_demo_spring_basic_init (AdwDemoSpringBasic *self)
+{
+  g_type_ensure (ADW_TYPE_DEMO_TRANSFORM_LAYOUT);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_drawing_area_set_draw_func (self->darea, (GtkDrawingAreaDrawFunc) draw_cb, self, NULL);
+
+  set_value (self, 0);
+  adw_demo_spring_basic_reset (self);
+}
+
+void
+adw_demo_spring_basic_reset (AdwDemoSpringBasic *self)
+{
+  if (self->animation)
+    adw_spring_animation_stop (self->animation);
+
+  if (self->graph_animation)
+    adw_spring_animation_stop (self->graph_animation);
+
+  g_clear_pointer (&self->points, g_array_unref);
+  self->points = NULL;
+  self->min = 0;
+  self->max = 1;
+
+  gtk_widget_set_opacity (self->label_box, 0);
+  gtk_widget_queue_draw (GTK_WIDGET (self->darea));
+}
diff --git a/demo/pages/spring/adw-demo-spring-basic.h b/demo/pages/spring/adw-demo-spring-basic.h
new file mode 100644
index 0000000..eb9058f
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-basic.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_SPRING_BASIC (adw_demo_spring_basic_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoSpringBasic, adw_demo_spring_basic, ADW, DEMO_SPRING_BASIC, AdwBin)
+
+void adw_demo_spring_basic_reset (AdwDemoSpringBasic *self);
+
+G_END_DECLS
diff --git a/demo/pages/spring/adw-demo-spring-basic.ui b/demo/pages/spring/adw-demo-spring-basic.ui
new file mode 100644
index 0000000..da1c1de
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-basic.ui
@@ -0,0 +1,274 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <template class="AdwDemoSpringBasic" parent="AdwBin">
+    <property name="child">
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <style>
+          <class name="content"/>
+        </style>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="transition-type">crossfade</property>
+            <property name="overflow">hidden</property>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">basic</property>
+                <property name="child">
+                  <object class="GtkFlowBox">
+                    <property name="homogeneous">True</property>
+                    <property name="min-children-per-line">1</property>
+                    <property name="max-children-per-line">2</property>
+                    <property name="selection-mode">none</property>
+                    <style>
+                      <class name="demo-grid"/>
+                    </style>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="homogeneous">True</property>
+                        <child>
+                          <object class="AdwBin">
+                            <property name="height-request">160</property>
+                            <property name="width-request">140</property>
+                            <property name="layout-manager">
+                              <object class="AdwDemoTransformLayout" id="scale_layout"/>
+                            </property>
+                            <property name="child">
+                              <object class="AdwBin">
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="scale"/>
+                                </style>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="AdwBin">
+                            <property name="height-request">160</property>
+                            <property name="width-request">140</property>
+                            <property name="layout-manager">
+                              <object class="AdwDemoTransformLayout" id="htranslate_layout"/>
+                            </property>
+                            <property name="child">
+                              <object class="AdwBin">
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="htranslate"/>
+                                </style>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="homogeneous">True</property>
+                        <child>
+                          <object class="AdwBin">
+                            <property name="height-request">160</property>
+                            <property name="width-request">140</property>
+                            <property name="layout-manager">
+                              <object class="AdwDemoTransformLayout" id="rotate_layout"/>
+                            </property>
+                            <property name="child">
+                              <object class="AdwBin">
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="rotate"/>
+                                </style>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="AdwBin">
+                            <property name="height-request">160</property>
+                            <property name="width-request">140</property>
+                            <property name="layout-manager">
+                              <object class="AdwDemoTransformLayout" id="vtranslate_layout"/>
+                            </property>
+                            <property name="child">
+                              <object class="AdwBin">
+                                <property name="halign">center</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="vtranslate"/>
+                                </style>
+                              </object>
+                            </property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStackPage">
+                <property name="name">info</property>
+                <property name="child">
+                  <object class="GtkOverlay">
+                    <child>
+                      <object class="GtkDrawingArea" id="darea"/>
+                    </child>
+                    <child type="overlay">
+                      <object class="GtkBox" id="label_box">
+                        <property name="valign">end</property>
+                        <property name="spacing">12</property>
+                        <property name="opacity">0</property>
+                        <child>
+                          <object class="GtkLabel" id="duration_label">
+                            <property name="hexpand">True</property>
+                            <property name="halign">start</property>
+                            <property name="margin-top">6</property>
+                            <property name="margin-bottom">6</property>
+                            <property name="margin-start">6</property>
+                            <style>
+                              <class name="numeric"/>
+                            </style>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="min_label">
+                            <property name="margin-top">6</property>
+                            <property name="margin-bottom">6</property>
+                            <style>
+                              <class name="numeric"/>
+                            </style>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="max_label">
+                            <property name="margin-top">6</property>
+                            <property name="margin-bottom">6</property>
+                            <property name="margin-end">6</property>
+                            <style>
+                              <class name="numeric"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSeparator"/>
+        </child>
+        <child>
+          <object class="AdwSqueezer">
+            <property name="homogeneous">False</property>
+            <property name="interpolate-size">True</property>
+            <property name="transition-type">crossfade</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <child>
+              <object class="GtkBox">
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="width-request">250</property>
+                    <property name="title" translatable="yes">Velocity</property>
+                    <property name="hexpand">True</property>
+                    <property name="adjustment">velocity</property>
+                    <property name="digits">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkToggleButton" id="info_btn">
+                    <property name="valign">center</property>
+                    <property name="icon-name">spring-graph-symbolic</property>
+                    <property name="margin-end">12</property>
+                    <signal name="notify::active" handler="active_changed_cb" swapped="true"/>
+                    <style>
+                      <class name="circular-large"/>
+                      <class name="list-button"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton">
+                    <property name="icon-name">spring-run-symbolic</property>
+                    <property name="valign">center</property>
+                    <property name="margin-end">18</property>
+                    <signal name="clicked" handler="run_cb" swapped="true"/>
+                    <style>
+                      <class name="circular-large"/>
+                      <class name="suggested-action"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="AdwDemoAdjustmentRow">
+                    <property name="title" translatable="yes">Velocity</property>
+                    <property name="hexpand">True</property>
+                    <property name="adjustment">velocity</property>
+                    <property name="digits">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkSeparator"/>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="spacing">12</property>
+                    <property name="halign">center</property>
+                    <property name="margin-top">12</property>
+                    <property name="margin-bottom">12</property>
+                    <property name="margin-start">12</property>
+                    <property name="margin-end">12</property>
+                    <child>
+                      <object class="GtkToggleButton">
+                        <property name="active" bind-source="info_btn" bind-property="active" 
bind-flags="bidirectional"/>
+                        <property name="valign">center</property>
+                        <property name="icon-name">spring-graph-symbolic</property>
+                        <style>
+                      <class name="circular-large"/>
+                          <class name="list-button"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="icon-name">spring-run-symbolic</property>
+                        <property name="valign">center</property>
+                        <signal name="clicked" handler="run_cb" swapped="true"/>
+                        <style>
+                          <class name="circular-large"/>
+                          <class name="suggested-action"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </property>
+  </template>
+  <object class="GtkAdjustment" id="velocity">
+    <property name="lower">-50</property>
+    <property name="upper">50</property>
+    <property name="value">0</property>
+    <property name="step-increment">1</property>
+    <property name="page-increment">10</property>
+    <signal name="value-changed" handler="adw_demo_spring_basic_reset" swapped="true"/>
+  </object>
+</interface>
diff --git a/demo/pages/spring/adw-demo-spring-interactive.c b/demo/pages/spring/adw-demo-spring-interactive.c
new file mode 100644
index 0000000..cc36943
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-interactive.c
@@ -0,0 +1,292 @@
+#include "adw-demo-spring-interactive.h"
+
+#include <glib/gi18n.h>
+
+#include "adw-demo-transform-layout.h"
+#include "adw-spring-animation-private.h"
+
+struct _AdwDemoSpringInteractive
+{
+  AdwBin parent_instance;
+
+  gdouble damping;
+  gdouble mass;
+  gdouble stiffness;
+  gdouble precision;
+
+  AdwDemoTransformLayout *layout;
+  GtkWidget *handle;
+  GtkGesture *drag_gesture;
+  GtkGesture *swipe_gesture;
+  GdkCursor *grab_cursor;
+  GdkCursor *grabbing_cursor;
+
+  AdwSpringAnimation *animation_x;
+  AdwSpringAnimation *animation_y;
+
+  gdouble start_x;
+  gdouble start_y;
+  gdouble last_x;
+  gdouble last_y;
+};
+
+G_DEFINE_TYPE (AdwDemoSpringInteractive, adw_demo_spring_interactive, ADW_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_DAMPING,
+  PROP_MASS,
+  PROP_STIFFNESS,
+  PROP_PRECISION,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+set_translation (AdwDemoSpringInteractive *self,
+                 gdouble                   x,
+                 gdouble                   y)
+{
+  self->last_x = x;
+  self->last_y = y;
+
+  adw_demo_transform_layout_take_transform (self->layout,
+                                            gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (x, y)));
+}
+
+static void
+x_value_cb (gdouble                   value,
+            AdwDemoSpringInteractive *self)
+{
+  set_translation (self, value, self->last_y);
+}
+
+static void
+x_done_cb (AdwDemoSpringInteractive *self)
+{
+  g_clear_pointer (&self->animation_x, adw_spring_animation_unref);
+}
+
+static void
+y_value_cb (gdouble                   value,
+            AdwDemoSpringInteractive *self)
+{
+  set_translation (self, self->last_x, value);
+}
+
+static void
+y_done_cb (AdwDemoSpringInteractive *self)
+{
+  g_clear_pointer (&self->animation_y, adw_spring_animation_unref);
+}
+
+static void
+animate (AdwDemoSpringInteractive *self,
+         gdouble                   velocity_x,
+         gdouble                   velocity_y)
+{
+  gtk_widget_set_cursor (self->handle,
+                         self->grab_cursor);
+
+  self->animation_x = adw_spring_animation_new (self->handle,
+                                                self->last_x,
+                                                0,
+                                                velocity_x,
+                                                self->damping,
+                                                self->mass,
+                                                self->stiffness,
+                                                self->precision,
+                                                (AdwAnimationValueCallback) x_value_cb,
+                                                (AdwAnimationDoneCallback) x_done_cb,
+                                                self);
+
+  self->animation_y = adw_spring_animation_new (self->handle,
+                                                self->last_y,
+                                                0,
+                                                velocity_y,
+                                                self->damping,
+                                                self->mass,
+                                                self->stiffness,
+                                                self->precision,
+                                                (AdwAnimationValueCallback) y_value_cb,
+                                                (AdwAnimationDoneCallback) y_done_cb,
+                                                self);
+
+  adw_spring_animation_start (self->animation_x);
+  adw_spring_animation_start (self->animation_y);
+}
+
+static void
+drag_begin_cb (AdwDemoSpringInteractive *self,
+               gdouble                   start_x,
+               gdouble                   start_y)
+{
+  if (gtk_widget_pick (GTK_WIDGET (self), start_x, start_y, GTK_PICK_DEFAULT) != self->handle) {
+    gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+
+  self->start_x = self->last_x;
+  self->start_y = self->last_y;
+
+  if (self->animation_x)
+    adw_spring_animation_stop (self->animation_x);
+
+  if (self->animation_y)
+    adw_spring_animation_stop (self->animation_y);
+
+  set_translation (self, self->start_x, self->start_y);
+
+  gtk_widget_set_cursor (self->handle,
+                         self->grabbing_cursor);
+}
+
+static void
+drag_update_cb (AdwDemoSpringInteractive *self,
+                gdouble                   offset_x,
+                gdouble                   offset_y)
+{
+  set_translation (self, offset_x + self->start_x, offset_y + self->start_y);
+}
+
+static void
+drag_cancel_cb (AdwDemoSpringInteractive *self)
+{
+  if (self->animation_x)
+    adw_spring_animation_stop (self->animation_x);
+
+  if (self->animation_y)
+    adw_spring_animation_stop (self->animation_y);
+
+  animate (self, 0, 0);
+}
+
+static void
+adw_demo_spring_interactive_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  AdwDemoSpringInteractive *self = ADW_DEMO_SPRING_INTERACTIVE (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    g_value_set_double (value, self->damping);
+    break;
+  case PROP_MASS:
+    g_value_set_double (value, self->mass);
+    break;
+  case PROP_STIFFNESS:
+    g_value_set_double (value, self->stiffness);
+    break;
+  case PROP_PRECISION:
+    g_value_set_double (value, self->precision);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_interactive_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  AdwDemoSpringInteractive *self = ADW_DEMO_SPRING_INTERACTIVE (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    self->damping = g_value_get_double (value);
+    break;
+  case PROP_MASS:
+    self->mass = g_value_get_double (value);
+    break;
+  case PROP_STIFFNESS:
+    self->stiffness = g_value_get_double (value);
+    break;
+  case PROP_PRECISION:
+    self->precision = g_value_get_double (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_interactive_class_init (AdwDemoSpringInteractiveClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = adw_demo_spring_interactive_get_property;
+  object_class->set_property = adw_demo_spring_interactive_set_property;
+
+  props[PROP_DAMPING] =
+    g_param_spec_double ("damping",
+                         _("Damping"),
+                         _("Damping"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_MASS] =
+    g_param_spec_double ("mass",
+                         _("Mass"),
+                         _("Mass"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_STIFFNESS] =
+    g_param_spec_double ("stiffness",
+                         _("Stiffness"),
+                         _("Stiffness"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_PRECISION] =
+    g_param_spec_double ("precision",
+                         _("Precision"),
+                         _("Precision"),
+                         0, 1, 0,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Adwaita/Demo/pages/spring/adw-demo-spring-interactive.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, layout);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, handle);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, drag_gesture);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, swipe_gesture);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, grab_cursor);
+  gtk_widget_class_bind_template_child (widget_class, AdwDemoSpringInteractive, grabbing_cursor);
+  gtk_widget_class_bind_template_callback (widget_class, drag_begin_cb);
+  gtk_widget_class_bind_template_callback (widget_class, drag_update_cb);
+  gtk_widget_class_bind_template_callback (widget_class, drag_cancel_cb);
+  gtk_widget_class_bind_template_callback (widget_class, animate);
+}
+
+static void
+adw_demo_spring_interactive_init (AdwDemoSpringInteractive *self)
+{
+  g_type_ensure (ADW_TYPE_DEMO_TRANSFORM_LAYOUT);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_gesture_group (self->drag_gesture, self->swipe_gesture);
+}
+
+void
+adw_demo_spring_interactive_reset (AdwDemoSpringInteractive *self)
+{
+  if (self->animation_x)
+    adw_spring_animation_stop (self->animation_x);
+
+  if (self->animation_y)
+    adw_spring_animation_stop (self->animation_y);
+
+  set_translation (self, 0, 0);
+}
diff --git a/demo/pages/spring/adw-demo-spring-interactive.h b/demo/pages/spring/adw-demo-spring-interactive.h
new file mode 100644
index 0000000..d0461a3
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-interactive.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <adwaita.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_SPRING_INTERACTIVE (adw_demo_spring_interactive_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoSpringInteractive, adw_demo_spring_interactive, ADW, DEMO_SPRING_INTERACTIVE, 
AdwBin)
+
+void adw_demo_spring_interactive_reset (AdwDemoSpringInteractive *self);
+
+G_END_DECLS
diff --git a/demo/pages/spring/adw-demo-spring-interactive.ui 
b/demo/pages/spring/adw-demo-spring-interactive.ui
new file mode 100644
index 0000000..a38a048
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-interactive.ui
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <template class="AdwDemoSpringInteractive" parent="AdwBin">
+    <property name="layout-manager">
+      <object class="AdwDemoTransformLayout" id="layout"/>
+    </property>
+    <property name="child">
+      <object class="GtkImage" id="handle">
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+        <property name="icon-name">spring-interactive-symbolic</property>
+        <property name="icon-size">large</property>
+        <property name="cursor">grab_cursor</property>
+        <style>
+          <class name="interactive"/>
+        </style>
+      </object>
+    </property>
+    <child>
+      <object class="GtkGestureDrag" id="drag_gesture">
+        <property name="propagation-phase">capture</property>
+        <signal name="drag-begin" handler="drag_begin_cb" swapped="true"/>
+        <signal name="drag-update" handler="drag_update_cb" swapped="true"/>
+        <signal name="cancel" handler="drag_cancel_cb" swapped="true"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkGestureSwipe" id="swipe_gesture">
+        <property name="propagation-phase">capture</property>
+        <signal name="swipe" handler="animate" swapped="true"/>
+      </object>
+    </child>
+  </template>
+  <object class="GdkCursor" id="grab_cursor">
+    <property name="name">grab</property>
+  </object>
+  <object class="GdkCursor" id="grabbing_cursor">
+    <property name="name">grabbing</property>
+  </object>
+</interface>
diff --git a/demo/pages/spring/adw-demo-spring-preset.c b/demo/pages/spring/adw-demo-spring-preset.c
new file mode 100644
index 0000000..9418396
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-preset.c
@@ -0,0 +1,174 @@
+#include "adw-demo-spring-preset.h"
+
+#include <glib/gi18n.h>
+
+struct _AdwDemoSpringPreset
+{
+  GObject parent_instance;
+
+  gchar *name;
+  gdouble damping;
+  gdouble mass;
+  gdouble stiffness;
+  gdouble precision;
+};
+
+G_DEFINE_TYPE (AdwDemoSpringPreset, adw_demo_spring_preset, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DAMPING,
+  PROP_MASS,
+  PROP_STIFFNESS,
+  PROP_PRECISION,
+  PROP_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static inline void
+set_string (gchar       **dest,
+            const gchar  *source)
+{
+  if (*dest)
+    g_free (*dest);
+
+  *dest = g_strdup (source);
+}
+
+static void
+adw_demo_spring_preset_finalize (GObject *object)
+{
+  AdwDemoSpringPreset *self = ADW_DEMO_SPRING_PRESET (object);
+
+  g_clear_pointer (&self->name, g_free);
+
+  G_OBJECT_CLASS (adw_demo_spring_preset_parent_class)->finalize (object);
+}
+
+static void
+adw_demo_spring_preset_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  AdwDemoSpringPreset *self = ADW_DEMO_SPRING_PRESET (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    g_value_set_double (value, self->damping);
+    break;
+  case PROP_MASS:
+    g_value_set_double (value, self->mass);
+    break;
+  case PROP_STIFFNESS:
+    g_value_set_double (value, self->stiffness);
+    break;
+  case PROP_PRECISION:
+    g_value_set_double (value, self->precision);
+    break;
+  case PROP_NAME:
+    g_value_set_string (value, self->name);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_preset_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  AdwDemoSpringPreset *self = ADW_DEMO_SPRING_PRESET (object);
+
+  switch (prop_id) {
+  case PROP_DAMPING:
+    self->damping = g_value_get_double (value);
+    break;
+  case PROP_MASS:
+    self->mass = g_value_get_double (value);
+    break;
+  case PROP_STIFFNESS:
+    self->stiffness = g_value_get_double (value);
+    break;
+  case PROP_PRECISION:
+    self->precision = g_value_get_double (value);
+    break;
+  case PROP_NAME:
+    set_string (&self->name, g_value_get_string (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_spring_preset_class_init (AdwDemoSpringPresetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = adw_demo_spring_preset_finalize;
+  object_class->get_property = adw_demo_spring_preset_get_property;
+  object_class->set_property = adw_demo_spring_preset_set_property;
+
+  props[PROP_DAMPING] =
+    g_param_spec_double ("damping",
+                         _("Damping"),
+                         _("Damping"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_MASS] =
+    g_param_spec_double ("mass",
+                         _("Mass"),
+                         _("Mass"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_STIFFNESS] =
+    g_param_spec_double ("stiffness",
+                         _("Stiffness"),
+                         _("Stiffness"),
+                         0, G_MAXDOUBLE, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_PRECISION] =
+    g_param_spec_double ("precision",
+                         _("Precision"),
+                         _("Precision"),
+                         0, 1, 0,
+                         G_PARAM_READWRITE);
+
+  props[PROP_NAME] =
+    g_param_spec_string ("name",
+                         _("Name"),
+                         _("Name"),
+                         NULL,
+                         G_PARAM_READWRITE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+adw_demo_spring_preset_init (AdwDemoSpringPreset *self)
+{
+}
+
+AdwDemoSpringPreset *
+adw_demo_spring_preset_new (gdouble      damping,
+                            gdouble      mass,
+                            gdouble      stiffness,
+                            gdouble      precision,
+                            const gchar *name)
+{
+  return g_object_new (ADW_TYPE_DEMO_SPRING_PRESET,
+                       "damping", damping,
+                       "mass", mass,
+                       "stiffness", stiffness,
+                       "precision", precision,
+                       "name", name,
+                       NULL);
+}
diff --git a/demo/pages/spring/adw-demo-spring-preset.h b/demo/pages/spring/adw-demo-spring-preset.h
new file mode 100644
index 0000000..af8190a
--- /dev/null
+++ b/demo/pages/spring/adw-demo-spring-preset.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_SPRING_PRESET (adw_demo_spring_preset_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoSpringPreset, adw_demo_spring_preset, ADW, DEMO_SPRING_PRESET, GObject)
+
+AdwDemoSpringPreset *adw_demo_spring_preset_new (gdouble      damping,
+                                                 gdouble      mass,
+                                                 gdouble      stiffness,
+                                                 gdouble      precision,
+                                                 const gchar *name);
+
+G_END_DECLS
diff --git a/demo/pages/spring/adw-demo-transform-layout.c b/demo/pages/spring/adw-demo-transform-layout.c
new file mode 100644
index 0000000..e79dea9
--- /dev/null
+++ b/demo/pages/spring/adw-demo-transform-layout.c
@@ -0,0 +1,190 @@
+#include "adw-demo-transform-layout.h"
+
+#include <glib/gi18n.h>
+
+struct _AdwDemoTransformLayout
+{
+  GtkLayoutManager parent_instance;
+
+  GskTransform *transform;
+};
+
+G_DEFINE_TYPE (AdwDemoTransformLayout, adw_demo_transform_layout, GTK_TYPE_LAYOUT_MANAGER)
+
+enum {
+  PROP_0,
+  PROP_TRANSFORM,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+adw_demo_transform_layout_measure (GtkLayoutManager *layout,
+                                   GtkWidget        *widget,
+                                   GtkOrientation    orientation,
+                                   gint              for_size,
+                                   gint             *minimum,
+                                   gint             *natural,
+                                   gint             *minimum_baseline,
+                                   gint             *natural_baseline)
+{
+  GtkWidget *child;
+
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child)) {
+    gint child_min = 0;
+    gint child_nat = 0;
+    gint child_min_baseline = -1;
+    gint child_nat_baseline = -1;
+
+    if (!gtk_widget_should_layout (child))
+      continue;
+
+    gtk_widget_measure (child, orientation, for_size,
+                        &child_min, &child_nat,
+                        &child_min_baseline, &child_nat_baseline);
+
+    *minimum = MAX (*minimum, child_min);
+    *natural = MAX (*natural, child_nat);
+
+    if (child_min_baseline > -1)
+      *minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
+    if (child_nat_baseline > -1)
+      *natural_baseline = MAX (*natural_baseline, child_nat_baseline);
+  }
+}
+
+static void
+adw_demo_transform_layout_allocate (GtkLayoutManager *layout,
+                                    GtkWidget        *widget,
+                                    gint              width,
+                                    gint              height,
+                                    gint              baseline)
+{
+  AdwDemoTransformLayout *self = ADW_DEMO_TRANSFORM_LAYOUT (layout);
+  GtkWidget *child;
+
+  for (child = gtk_widget_get_first_child (widget);
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child)) {
+    GskTransform *transform;
+
+    if (!child || !gtk_widget_should_layout (child))
+      continue;
+
+    transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (width / 2.0f, height / 2.0f));
+    transform = gsk_transform_transform (transform, gsk_transform_ref (self->transform));
+    transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (-width / 2.0f, -height / 2.0f));
+
+    gtk_widget_allocate (child, width, height, baseline, transform);
+  }
+}
+
+static void
+adw_demo_transform_layout_dispose (GObject *object)
+{
+  AdwDemoTransformLayout *self = ADW_DEMO_TRANSFORM_LAYOUT (object);
+
+  g_clear_pointer (&self->transform, gsk_transform_unref);
+
+  G_OBJECT_CLASS (adw_demo_transform_layout_parent_class)->dispose (object);
+}
+
+static void
+adw_demo_transform_layout_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  AdwDemoTransformLayout *self = ADW_DEMO_TRANSFORM_LAYOUT (object);
+
+  switch (prop_id) {
+  case PROP_TRANSFORM:
+    g_value_set_boxed (value, adw_demo_transform_layout_get_transform (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_transform_layout_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  AdwDemoTransformLayout *self = ADW_DEMO_TRANSFORM_LAYOUT (object);
+
+  switch (prop_id) {
+  case PROP_TRANSFORM:
+    adw_demo_transform_layout_set_transform (self, g_value_get_boxed (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_demo_transform_layout_class_init (AdwDemoTransformLayoutClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkLayoutManagerClass *layout_class = GTK_LAYOUT_MANAGER_CLASS (klass);
+
+  object_class->dispose = adw_demo_transform_layout_dispose;
+  object_class->get_property = adw_demo_transform_layout_get_property;
+  object_class->set_property = adw_demo_transform_layout_set_property;
+
+  layout_class->measure = adw_demo_transform_layout_measure;
+  layout_class->allocate = adw_demo_transform_layout_allocate;
+
+  props[PROP_TRANSFORM] =
+      g_param_spec_boxed ("transform",
+                          _("Transform"),
+                          _("Transform"),
+                          GSK_TYPE_TRANSFORM,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+adw_demo_transform_layout_init (AdwDemoTransformLayout *self)
+{
+}
+
+GskTransform *
+adw_demo_transform_layout_get_transform (AdwDemoTransformLayout *self)
+{
+  g_return_val_if_fail (ADW_IS_DEMO_TRANSFORM_LAYOUT (self), NULL);
+
+  return self->transform;
+}
+
+void
+adw_demo_transform_layout_set_transform (AdwDemoTransformLayout *self,
+                                         GskTransform           *transform)
+{
+  g_return_if_fail (ADW_IS_DEMO_TRANSFORM_LAYOUT (self));
+
+  if (transform == self->transform)
+    return;
+
+  gsk_transform_unref (self->transform);
+  self->transform = gsk_transform_ref (transform);
+
+  gtk_layout_manager_layout_changed (GTK_LAYOUT_MANAGER (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSFORM]);
+}
+
+void
+adw_demo_transform_layout_take_transform (AdwDemoTransformLayout *self,
+                                          GskTransform           *transform)
+{
+  g_return_if_fail (ADW_IS_DEMO_TRANSFORM_LAYOUT (self));
+
+  adw_demo_transform_layout_set_transform (self, transform);
+  gsk_transform_unref (transform);
+}
diff --git a/demo/pages/spring/adw-demo-transform-layout.h b/demo/pages/spring/adw-demo-transform-layout.h
new file mode 100644
index 0000000..dc263a7
--- /dev/null
+++ b/demo/pages/spring/adw-demo-transform-layout.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_TRANSFORM_LAYOUT (adw_demo_transform_layout_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoTransformLayout, adw_demo_transform_layout, ADW, DEMO_TRANSFORM_LAYOUT, 
GtkLayoutManager)
+
+GskTransform *adw_demo_transform_layout_get_transform (AdwDemoTransformLayout *self);
+void          adw_demo_transform_layout_set_transform (AdwDemoTransformLayout *self,
+                                                       GskTransform           *transform);
+
+void          adw_demo_transform_layout_take_transform (AdwDemoTransformLayout *self,
+                                                        GskTransform           *transform);
+
+G_END_DECLS
diff --git a/demo/style.css b/demo/style.css
index 7e8b2b9..5091b0a 100644
--- a/demo/style.css
+++ b/demo/style.css
@@ -1,3 +1,84 @@
+@define-color red_1 #f66151;
+@define-color red_3 #e01b24;
+@define-color green_2 #57e389;
+@define-color green_5 #26a269;
+@define-color blue_2 #62a0ea;
+@define-color blue_4 #1c71d8;
+@define-color yellow_1 #f9f06b;
+@define-color yellow_2 #f8e45c;
+@define-color yellow_5 #e5a50a;
+
+/* Welp */
+window.maximized {
+  box-shadow: none;
+}
+
+.htranslate,
+.vtranslate,
+.scale {
+  min-width: 30px;
+  min-height: 30px;
+  border-radius: 24px;
+  background: #33d17a;
+}
+
+.scale {
+  background: linear-gradient(to bottom, @blue_2, @blue_4);
+}
+
+.htranslate {
+  background: linear-gradient(to bottom, @green_2, @green_5);
+}
+
+.vtranslate {
+  background: linear-gradient(to bottom, @yellow_2, @yellow_5);
+}
+
+.rotate {
+  min-width: 54px;
+  min-height: 90px;
+  border-radius: 8px;
+  background: linear-gradient(to bottom, @red_1, @red_3);
+}
+
+.interactive {
+  min-width: 96px;
+  min-height: 96px;
+  border-radius: 48px;
+  background: linear-gradient(to bottom, @red_1, @red_3);
+  color: white;
+}
+
+stack.content {
+  border: 1px solid alpha(@borders, .7);
+  border-radius: 8px;
+  background: @theme_base_color;
+}
+
+flowbox.content {
+  background: @theme_base_color;
+  border-radius: 8px;
+  border: 1px solid alpha(@borders, .7);
+  background: alpha(@borders, .7);
+  background-clip: padding-box;
+}
+
+flowbox.content > flowboxchild {
+  background: @theme_base_color;
+}
+
+.numeric {
+  font-feature-settings: "tnum";
+}
+
+.circular-large {
+  min-width: 50px;
+  min-height: 50px;
+  padding: 0;
+  border-radius: 100px;
+  -gtk-icon-size: 24px;
+}
+
 .numeric {
   font-feature-settings: "tnum";
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]