1

I've been playing around with the CatmullRomSpline class from Flutter the past few days. Drawing points with is trivial, however, I've been looking for information on interpolating between two sets of values and haven't had much luck.

to give a better overview here is the first plot of offsets: 1

This is the next plot of offsets that I would like to morph/animate to:

2

Here is the code I have so far:

import 'dart:ui';

import 'package:curves/data.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'models/price_plot.dart';

void main() {
  runApp(const App());
}

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({
    Key? key,
  }) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  late Animation<List<Offset>> animation;
  late AnimationController controller;

  List<Offset> controlPoints = [];

  @override
  void initState() {
    super.initState();

    WidgetsBinding.instance.addPostFrameCallback((_) {
      controlPoints = buildControlPoints(hourItems);

      setState(() {});
    });
  }

  List<Offset> buildControlPoints(List<List<double>> items) {
    final max = items.map((x) => x.last).fold<double>(
          items.first.last,
          math.max,
        );
    final min = items.map((x) => x.last).fold<double>(
          items.first.last,
          math.min,
        );

    final range = max - min;

    final priceMap = {
      for (var v in items)
        v.last: ((max - v.last) / range) * MediaQuery.of(context).size.height,
    };

    final pricePlots =
        priceMap.entries.map((e) => PricePlot(e.value.toInt(), e.key)).toList();

    return controlPoints = List.generate(
      pricePlots.length,
      (index) {
        return Offset(
          calculateXOffset(
              MediaQuery.of(context).size.width, pricePlots, index),
          pricePlots[index].yAxis.toDouble(),
        );
      },
    ).toList();
  }

  double calculateXOffset(double width, List<PricePlot> list, int index) {
    if (index == (list.length - 1)) {
      return width;
    }

    return index == 0 ? 0 : width ~/ list.length * (index + 1);
  }

  void _startAnimation() {
    controller.stop();
    controller.reset();
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("App"),
      ),
      body: Stack(
        children: [
          CustomPaint(
            painter: SplinePainter(controlPoints),
            size: Size(MediaQuery.of(context).size.width,
                MediaQuery.of(context).size.height),
          ),
          FloatingActionButton(onPressed: () {
            _startAnimation();
          })
        ],
      ),
    );
  }
}

class SplinePainter extends CustomPainter {
  final List<Offset> items;

  SplinePainter(this.items);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawPaint(Paint()..color = Colors.white);

    if (items.isEmpty) {
      return;
    }

    final spline = CatmullRomSpline(items);

    final bezierPaint = Paint()
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 2
      ..color = Colors.blue;

    canvas.drawPoints(
      PointMode.points,
      spline.generateSamples(tolerance: 1e-17).map((e) => e.value).toList(),
      bezierPaint,
    );
  }

  @override
  bool shouldRepaint(SplinePainter oldDelegate) => true;
}

1 Answer 1

1

not coding in your environment but I would try:

  1. lets have p0[n0] and p1[n1] control points for your curves

  2. let n = max(n0,n1)

  3. resample the less sampled curve to n points

  4. linearly interpolate between corresponding curve points

    p[i] = p0[i] + (p1[i]-p0[i])*t
    

    where t is linear parameter in range <0.0,1.0>

So to animate just render p[n] with t=0.0 wait a bit, increase tby small amount render again and so on until you hit t=1.0 ...

If you share your control point I would make a simple C++ example and grab a GIF animation but without I would need to extract the points from image which is a problem as you did not mark control point in your plot... and the extraction is much more work than the resampling and animation itself...

After your comments and looking at the link of yours its obvious you do not have 2 datasets but just one and just changing the sampling rate ...

So I taken the more dense data you provided and create second set by skipping points ... so booth sets covering the same time interval just have different number of points but still uniformly sampled...

Putting all together I created a small C++/VCL example of doing this:

//$$---- Form CPP ----
//---------------------------------------------------------------------------
#include <vcl.h>
#include "List.h"
#include "str.h"
#pragma hdrstop
#include "win_main.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
// https://stackoverflow.com/a/66565537/2521214
//---------------------------------------------------------------------------
int xs,ys;              // screen resolution
Graphics::TBitmap *bmp; // back buffer bitmap for rendering
const int N=1024;
List<double> p0,p1,p;
double x0,y0,x1,y1,mx,my;
//---------------------------------------------------------------------------
void dat_load(List<double> &p,AnsiString filename)
    {
    BYTE *dat;
    int hnd,siz,adr,i;
    AnsiString s,ss;
    p.reset();
    hnd=FileOpen(filename,fmOpenRead);
    if (hnd==-1) return;
    siz=FileSeek(hnd,0,2);
        FileSeek(hnd,0,0);
    dat=new BYTE[siz];
    if (dat==NULL){ FileClose(hnd); return; }
    FileRead(hnd,dat,siz);
    FileClose(hnd);
    for (adr=0;adr<siz;)
        {
        s=txt_load_str('[',']',dat,siz,adr,true);
        if (s=="") break;
        if (s[1]=='[') s[1]=' ';
        i=1;
        p.add(str2num(str_load_str(s,i,true)));
        p.add(str2num(str_load_str(s,i,true)));
        if (s[s.Length()]==']') break;;
        }
    delete[] dat;
    }
//---------------------------------------------------------------------------
void dat_getpnt(double &x,double &y,double *p,int n,double t)
    {
    int i0,i1,i2,i3;
    double tt,ttt,a0,a1,a2,a3,d1,d2;
    // bount parameter
    if (t<0.0) t=0.0;
    if (t>  n) t=n;
    // control points indexes
    i1=floor(t); i1&=0xFFFFFFFE;
    t=(t-i1)*0.5; tt*t*t; ttt=tt*t;
             if (i1>=n) i1=n-2;
    i0=i1-2; if (i0< 0) i0=  0;
    i2=i1+2; if (i2>=n) i2=n-2;
    i3=i2+2; if (i3>=n) i3=n-2;
    // cubic x
    d1=0.5*(p[i2]-p[i0]);
    d2=0.5*(p[i3]-p[i1]);
    a0=p[i1];
    a1=d1;
    a2=(3.0*(p[i2]-p[i1]))-(2.0*d1)-d2;
    a3=d1+d2+(2.0*(-p[i2]+p[i1]));
    x=a0+a1*t+a2*t*t+a3*t*t*t;
    // cubic y
    i0++; i1++; i2++; i3++;
    d1=0.5*(p[i2]-p[i0]);
    d2=0.5*(p[i3]-p[i1]);
    a0=p[i1];
    a1=d1;
    a2=(3.0*(p[i2]-p[i1]))-(2.0*d1)-d2;
    a3=d1+d2+(2.0*(-p[i2]+p[i1]));
    y=a0+a1*t+a2*t*t+a3*t*t*t;
    }
//---------------------------------------------------------------------------
void dat_draw(double *p,int n)
    {
    int e;
    double x,y,t,N=n;
    for (e=1,t=0.0;e;t+=0.1)
        {
        if (t>=N){ e=0; t=N; }
        dat_getpnt(x,y,p,n,t);
        x=x-x0; x*=mx;
        y=y-y0; y*=my;
        if (e==1){ bmp->Canvas->MoveTo(x,y); e=2; }
        else       bmp->Canvas->LineTo(x,y);
        }
    }
//---------------------------------------------------------------------------
void dat_draw(double *p0,int n0,double *p1,int n1,double t)
    {
    int i,N;
    double x,y,xx0,yy0,xx1,yy1,t0,t1,dt0,dt1;
    N=n0; if (N<n1) N=n1;
    dt0=double(n0)/double(N-1);
    dt1=double(n1)/double(N-1);
    for (t0=t1=0.0,i=0;i<N;i++,t0+=dt0,t1+=dt1)
        {
        dat_getpnt(xx0,yy0,p0,n0,t0);
        dat_getpnt(xx1,yy1,p1,n1,t1);
        x=xx0+((xx1-xx0)*t);
        y=yy0+((yy1-yy0)*t);
        x=x-x0; x*=mx;
        y=y-y0; y*=my;
        if (i==0) bmp->Canvas->MoveTo(x,y);
        else      bmp->Canvas->LineTo(x,y);
        }
    }
//---------------------------------------------------------------------------
void draw() // just render of my App
    {
    bmp->Canvas->Brush->Color=clWhite;
    bmp->Canvas->FillRect(TRect(0,0,xs,ys));

    bmp->Canvas->Pen->Color=clLtGray; dat_draw(p0.dat,p0.num);
    bmp->Canvas->Pen->Color=clLtGray; dat_draw(p1.dat,p1.num);
    static double t=0.0,dt=0.1;
    bmp->Canvas->Pen->Color=clBlue;   dat_draw(p0.dat,p0.num,p1.dat,p1.num,t);
    t+=dt; if ((dt>0.0)&&(t>=1.0)){ t=1.0; dt=-dt; }
           if ((dt<0.0)&&(t<=0.0)){ t=0.0; dt=-dt; }

    Form1->Canvas->Draw(0,0,bmp);
//  bmp->SaveToFile("out.bmp");
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner) // init of my app
    {
    // init backbuffer
    bmp=new Graphics::TBitmap;
    bmp->HandleType=bmDIB;
    bmp->PixelFormat=pf32bit;

    // load data
    dat_load(p0,"in.txt");
    // compute BBOX and create smaller dataset
    int i; double x,y;
    for (i=0;i<p0.num;)
        {
        x=p0[i]; i++;
        y=p0[i]; i++;
        if (i==2){ x0=x; y0=y; x1=x; y1=y; }
        if (x0>x) x0=x; if (x1<x) x1=x;
        if (y0>y) y0=y; if (y1<y) y1=y;
        if (i%60==2){ p1.add(x); p1.add(y); }
        }
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender) // not important just destructor of my App
    {
    delete bmp;
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender) // not important just resize event
    {
    xs=ClientWidth;
    ys=ClientHeight;
    bmp->Width=xs;
    bmp->Height=ys;
    mx=xs/(x1-x0);
    my=ys/(y1-y0);
    draw();
    }
//-------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender) // not important just repaint event
    {
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
    {
    draw();
    }
//---------------------------------------------------------------------------

You can ignore most of the stuff, the only important parts of code are functions:

void dat_getpnt(double &x,double &y,double *p,int n,double t);
void dat_draw(double *p0,int n0,double *p1,int n1,double t);

the first just obtains point on piecewise cubic curve p[n] with parameter t=<0.0,n>. The other one uses the first function to obtain corresponding points on both curve and linearly interpolate between them with parameter t=<0.0,1.0> to render interpolated curve.

The function void draw(); render the graph and animate the interpolation parameter ... its called in timer with 100ms interval...

Beware my data is just 1D array of x,y coordinates so n0,n1 are twice the number of points !!!

Here preview:

preview

The graph transition in the link of yours adds also animation of scale and scroll of time (x axis) but that is just a matter of changing view parameters ...

Sign up to request clarification or add additional context in comments.

8 Comments

Thanks for the information! Here is where I get the sample data from: coindesk.com/pf/api/v3/content/fetch/…
@DanielHakimi that is single or both curves?
Apologies, here is the other data set: coindesk.com/pf/api/v3/content/fetch/…
Just incase you want to fiddle with more data: coindesk.com/price/bitcoin
@DanielHakimi how do you want to handle time? those 2 datasets overlaps only a little the 1 day step is just very small part of the 1 minute set ... so you want to interpolate only corresponding time?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.