Bresenham tasks in ATS

From Rosetta Code

The following tasks are implemented here in ATS:

You will also need the source files from Bitmap#ATS and Bitmap/Write_a_PPM_file#ATS.

The ATS static file

The following file should be named bitmap_bresenham_tasks.sats.

#define ATS_PACKNAME "Rosetta_Code.bresenham_tasks"

%{#
#include <math.h>
%}

staload "bitmap_task.sats"

fn {a : t@ype} {tk : tkind}
pixmap_draw_line_endpoints_bresenham
          (pix    : !pixmap a,
           endpt0 : @(g0int tk, g0int tk),
           endpt1 : @(g0int tk, g0int tk),
           color  : a) : void

fn {a : t@ype} {tk : tkind}
pixmap_draw_circle_midpoint_algorithm
          (pix    : !pixmap a,
           midpt  : @(g0int tk, g0int tk),
           radius : g0int tk,
           color  : a) : void

fn {a : t@ype} {tk : tkind}
pixmap_draw_bezier_degree2_control_points
          (pix     : !pixmap a,
           ctrlpt0 : @(g0float tk, g0float tk),
           ctrlpt1 : @(g0float tk, g0float tk),
           ctrlpt2 : @(g0float tk, g0float tk),
           color   : a) : void

fn {a : t@ype} {tk : tkind}
pixmap_draw_bezier_degree3_control_points
          (pix     : !pixmap a,
           ctrlpt0 : @(g0float tk, g0float tk),
           ctrlpt1 : @(g0float tk, g0float tk),
           ctrlpt2 : @(g0float tk, g0float tk),
           ctrlpt3 : @(g0float tk, g0float tk),
           color   : a) : void

overload pixmap_draw_line with pixmap_draw_line_endpoints_bresenham
overload pixmap_draw_circle with pixmap_draw_circle_midpoint_algorithm

overload pixmap_draw_bezier_degree2 with
  pixmap_draw_bezier_degree2_control_points
overload pixmap_draw_bezier_degree3 with
  pixmap_draw_bezier_degree3_control_points
overload pixmap_draw_bezier with pixmap_draw_bezier_degree2
overload pixmap_draw_bezier with pixmap_draw_bezier_degree3

overload draw_line with pixmap_draw_line
overload draw_circle with pixmap_draw_circle
overload draw_bezier with pixmap_draw_bezier

The ATS dynamic file

The following file should be named bitmap_bresenham_tasks.dats.

(*------------------------------------------------------------------*)

#define ATS_DYNLOADFLAG 0
#define ATS_PACKNAME "Rosetta_Code.bresenham_tasks"

#include "share/atspre_staload.hats"

staload "bitmap_task.sats"
staload _ = "bitmap_task.dats"

staload "bitmap_bresenham_tasks.sats"

(*------------------------------------------------------------------*)
(* The ats2-xprelude package implements these, but for this task we
   will implement them ourselves: *)

extern castfn llint2size : {i : nat} llint i -<> size_t i
implement g1int2uint<llintknd,sizeknd> i = llint2size i

extern castfn size2llint : {i : int} size_t i -<> llint i
implement g1uint2int<sizeknd,llintknd> i =
  let prval () = lemma_g1uint_param i in size2llint i end

(*------------------------------------------------------------------*)

(* The ats2-xprelude package has "sqrt" that selects the correct
   function for a given floating point type. However, for this Rosetta
   Code task, I am avoiding the dependency. We will define it for
   double, only. *)

extern fn {tk : tkind} g0float_sqrt : g0float tk -<> g0float tk
implement g0float_sqrt<dblknd> x = $extfcall (double, "sqrt", x)

overload sqrt with g0float_sqrt

(*------------------------------------------------------------------*)
(* "Bisection iterators" -- a way to avoid using extra space when
   doing any of many adaptive algorithms.

   In ats2-xprelude, this facility is provided for exact rational
   numbers, going either forwards or backwards. Here we will use
   ullint and go only forwards. (WARNING: Using fixed size integers
   causes some risk of overflow. This risk is similar to that, in a
   recursive implementation, of using up all your stack.)  *)

typedef bisect_iter = @{n = ullint, d = ullint}

fn {}
bisect_iter_make () :<> bisect_iter =
  @{n = 0LLU, d = 1LLU}

fn {}
bisect_iter_done (iter : bisect_iter) :<> bool =
  (iter.n = iter.d && iter.d = 1LLU)

fn {tk : tkind}
bisect_iter_interval (iter : bisect_iter)
    :<> @(g0float tk, g0float tk) =
  (* Get the endpoints of the current interval, as floating point
     numbers between zero and one (inclusive). *)
  let
    (* The following works only for floating point types built into
       C. For other types (such as mpfr), you may get a WRONG
       result. *)
    extern castfn ulli2f : ullint -<> g0float tk
  in
    @(ulli2f (iter.n) / ulli2f (iter.d),
      ulli2f (succ (iter.n)) / ulli2f (iter.d))
  end

fn {}
bisect_iter_bisect (iter : bisect_iter) : bisect_iter =
  (* Bisect the current interval. *)
  @{n = (iter.n << 1), d = (iter.d << 1)}

fn {}
bisect_iter_next (iter : bisect_iter) : bisect_iter =
  (* Advance to the next interval. *)
  let
    extern castfn ull2ll : ullint -<> llint

    val n = succ (iter.n)

    (* WARNING: __builtin_ffsll is a GNU C extension, available also
                with some other compilers people use with ATS. If you
                need to, you can write a replacement for your system
                (or take one from Gnulib). See
                https://en.wikipedia.org/w/index.php?title=Find_first_set&oldid=1139170019
                and
                https://www.chessprogramming.org/BitScan#Bitscan_reverse
                *)
    val shift_amount : intGte 0 =
      pred ($extfcall (intGte 1, "__builtin_ffsll", ull2ll n))
  in
    @{n = (n >> shift_amount), d = ((iter.d) >> shift_amount)}
  end

(*------------------------------------------------------------------*)
(* Bresenham's line-drawing algorithm. See
https://en.wikipedia.org/w/index.php?title=Bresenham%27s_line_algorithm&oldid=1145813731

(This implementation may produce a fair bit of C code. I was not
parsimonious.) *)

extern fn {a : t@ype}
pixmap_draw_line$plot :
  {w, h : int}
  {x, y : nat | x < w; y < h}
  (!pixmap (a, w, h), size_t x, size_t y, a) -> void

fn {a : t@ype} {tk : tkind}
pixmap_draw_line_shallow
          {w, h  : int}
          {x0, y0, x1, y1 : int | x0 <= x1; y0 <= y1;
                                  y1 - y0 <= x1 - x0}
          (pix   : !pixmap (a, w, h),
           x0    : g1int (tk, x0),
           y0    : g1int (tk, y0),
           x1    : g1int (tk, x1),
           y1    : g1int (tk, y1),
           color : a)
    : void =
  let
    val w : size_t w = width pix
    and h : size_t h = height pix

    val dx = x1 - x0
    and dy = y1 - y0
    val twice_dx = dx + dx
    and twice_dy = dy + dy
    val twice_dy_minus_dx = twice_dy - twice_dx

    fun
    loop {x, y : int | x <= x1; y <= y1}
         {p    : addr}
         .<x1 - x>.
         (pix : !pixmap (a, w, h, p),
          x   : g1int (tk, x),
          y   : g1int (tk, y),
          D   : g1int tk) : void =
      begin
        if (g1i2i 0 <= x) * (x < g1u2i w)
              * (g1i2i 0 <= y) * (y < g1u2i h) then
          pixmap_draw_line$plot<a> (pix, g1i2u x, g1i2u y, color);
        if x <> x1 then
          begin
            if g1i2i 0 < D then
              (* The call to "min" here is to provide a proof that the
                 value is no greater than y1. If you can come up with
                 an actual proof, then go ahead and get rid of the
                 "min". *)
              loop (pix, succ x, min (succ y, y1),
                    D + twice_dy_minus_dx)
            else
              loop (pix, succ x, y, D + twice_dy)
          end
      end
  in
    loop (pix, x0, y0, twice_dy - dx)
  end

fn {a : t@ype} {tk : tkind}
pixmap_draw_line_steep
          {w, h  : int}
          {x0, y0, x1, y1 : int | x0 <= x1; y0 <= y1;
                                  x1 - x0 <= y1 - y0}
          (pix   : !pixmap (a, w, h),
           x0    : g1int (tk, x0),
           y0    : g1int (tk, y0),
           x1    : g1int (tk, x1),
           y1    : g1int (tk, y1),
           color : a)
    : void =
  let
    val w : size_t w = width pix
    and h : size_t h = height pix

    val dx = x1 - x0
    and dy = y1 - y0
    val twice_dx = dx + dx
    and twice_dy = dy + dy
    val twice_dx_minus_dy = twice_dx - twice_dy

    fun
    loop {x, y : int | x <= x1; y <= y1}
         {p    : addr}
         .<y1 - y>.
         (pix : !pixmap (a, w, h, p),
          x   : g1int (tk, x),
          y   : g1int (tk, y),
          D   : g1int tk) : void =
      begin
        if (g1i2i 0 <= x) * (x < g1u2i w)
              * (g1i2i 0 <= y) * (y < g1u2i h) then
          pixmap_draw_line$plot<a> (pix, g1i2u x, g1i2u y, color);
        if y <> y1 then
          begin
            if g1i2i 0 < D then
              (* The call to "min" here is to provide a proof that the
                 value is no greater than x1. If you can come up with
                 an actual proof, then go ahead and get rid of the
                 "min". *)
              loop (pix, min (succ x, x1), succ y,
                    D + twice_dx_minus_dy)
            else
              loop (pix, x, succ y, D + twice_dx)
          end
      end
  in
    loop (pix, x0, y0, twice_dx - dy)
  end

implement {a} {tk}
pixmap_draw_line_endpoints_bresenham (pix, @(x0, y0), @(x1, y1),
                                      color) =
  let
    macdef draw_shallow = pixmap_draw_line_shallow<a>
    macdef draw_steep = pixmap_draw_line_steep<a>

    val x0 = g1ofg0 x0 and y0 = g1ofg0 y0
    and x1 = g1ofg0 x1 and y1 = g1ofg0 y1
  in
    if x0 <= x1 then
      begin
        if y0 <= y1 then
          let
            implement
            pixmap_draw_line$plot<a> (pix, x, y, color) =
              pix[x, y] := color
          in
            if y1 - y0 <= x1 - x0 then
              draw_shallow (pix, x0, y0, x1, y1, color)
            else
              draw_steep (pix, x0, y0, x1, y1, color)
          end
        else
          let
            implement
            pixmap_draw_line$plot<a> (pix, x, y, color) =
              let
                val h1 = pred (height pix)
              in
                pix[x, h1 - y] := color
              end

            val h1 : g1int tk = pred (g1u2i (height pix))
          in
            if y0 - y1 <= x1 - x0 then
              draw_shallow (pix, x0, h1 - y0, x1, h1 - y1, color)
            else
              draw_steep (pix, x0, h1 - y0, x1, h1 - y1, color)
          end
      end
    else
      begin
        if y0 <= y1 then
          let
            implement
            pixmap_draw_line$plot<a> (pix, x, y, color) =
              let
                val w1 = pred (width pix)
              in
                pix[w1 - x, y] := color
              end

            val w1 : g1int tk = pred (g1u2i (width pix))
          in
            if y1 - y0 <= x0 - x1 then
              draw_shallow (pix, w1 - x0, y0, w1 - x1, y1, color)
            else
              draw_steep (pix, w1 - x0, y0, w1 - x1, y1, color)
          end
        else
          let
            implement
            pixmap_draw_line$plot<a> (pix, x, y, color) =
              let
                val w1 = pred (width pix)
                and h1 = pred (height pix)
              in
                pix[w1 - x, h1 - y] := color
              end

            val w1 : g1int tk = pred (g1u2i (width pix))
            and h1 : g1int tk = pred (g1u2i (height pix))
          in
            if y0 - y1 <= x0 - x1 then
              draw_shallow (pix, w1 - x0, h1 - y0,
                            w1 - x1, h1 - y1, color)
            else
              draw_steep (pix, w1 - x0, h1 - y0,
                          w1 - x1, h1 - y1, color)
          end
      end
  end

(*------------------------------------------------------------------*)
(* The midpoint algorithm for drawing a circle.

   There are several closely related algorithms. The one I am using is
   called "Midpoint Method" at https://archive.ph/g0Z9e (although that
   presentation contains errors), and seems to be what most others
   have been using.

   The Wikipedia article is, at the time of this writing, featuring
   "Jesko's Method". Presumably the page used to feature the "Midpoint
   Method" proper.

   I might mention that, when I use Wikipedia as a reference, I use
   the constant link for a page. Pages not only change--they can
   completely disappear. But constant links remain. Alternatively one
   can save a page at archive.today or the Wayback Machine. *)

implement {a} {tk}
pixmap_draw_circle_midpoint_algorithm (pix, @(x0, y0), radius,
                                       color) =
  let
    macdef zero = g1i2i 0
    macdef one = g1i2i 1

    val radius = g1ofg0 radius
    val radius = abs radius
    prval [radius : int] EQINT () = eqint_make_gint radius
    prval () = prop_verify {0 <= radius} ()

    val [x0 : int] x0 = g1ofg0 x0
    and [y0 : int] y0 = g1ofg0 y0

    fn {}
    plot {x, y : int}
         {w, h : int}
         {p    : addr}
         (pix  : !pixmap (a, w, h, p),
          x    : g1int (tk, x),
          y    : g1int (tk, y)) : void =
      if (zero <= x) * (x < g1u2i (width pix))
            * (zero <= y) * (y < g1u2i (height pix)) then
        pix[x, y] := color

    fun
    loop {w, h : int}
         {p    : addr}
         {x, y : int | y <= x + 2}
         .<(x + 2) - y>.
         (pix  : !pixmap (a, w, h, p),
          x    : g1int (tk, x),
          y    : g1int (tk, y),
          P    : g1int tk) : void =
      if y <= x then
        begin
          plot (pix, x0 - x, y0 - y);
          plot (pix, x0 - x, y0 + y);
          plot (pix, x0 + x, y0 - y);
          plot (pix, x0 + x, y0 + y);
          plot (pix, x0 - y, y0 - x);
          plot (pix, x0 - y, y0 + x);
          plot (pix, x0 + y, y0 - x);
          plot (pix, x0 + y, y0 + x);
          let
            val y = succ y
          in
            if P <= zero then
              loop (pix, x, y, succ (P + y + y))
            else
              let
                val x = pred x
                val diff = y - x
              in
                loop (pix, x, y, succ (P + diff + diff))
              end
          end
        end
  in
    loop (pix, radius, zero, one - radius)
  end

(*------------------------------------------------------------------*)
(* Symmetric power (Sánchez-Reyes) representation of Bézier
   splines. S-power representation has the advantage that
   coefficients, other than those representing the endpoints, tend
   towards zero as the curve gets flatter.

   References:

     J. Sánchez-Reyes, ‘The symmetric analogue of the polynomial power
         basis’, ACM Transactions on Graphics, vol 16 no 3, July 1997,
         page 319.

     J. Sánchez-Reyes, ‘Applications of the polynomial s-power basis
         in geometry processing’, ACM Transactions on Graphics, vol 19
         no 1, January 2000, page 35.

*)

(* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - *)
(* Degree 2. *)

fn {tk : tkind}
bernstein2spower_degree2
          @(c0 : g0float tk, c1 : g0float tk, c2 : g0float tk)
    :<> @(g0float tk, g0float tk, g0float tk) =
  (* Convert from Bernstein coefficients ("control points") to s-power
     coefficients. *)
  let
    (* First convert to scaled Bernstein basis. *)
    val c1 = c1 + c1
  in
    (* Then to s-power basis. *)
    @(c0, c1 - c0 - c2, c2)
  end

fn {tk : tkind}
spower_eval_degree2
          (@(c0 : g0float tk, c1 : g0float tk, c2 : g0float tk),
           t : g0float tk)
    :<> g0float tk =
  (* Evaluate the polynomial spower(c0, c1, c2) at t. (This function
     is used in the test code.) *)
  let
    val one_minus_t = g0i2f 1 - t
    val s = t * one_minus_t
    val c1_s = c1 * s
    val y0 = c0 + c1_s and y1 = c2 + c1_s
  in
    (y0 * one_minus_t) + (y1 * t)
  end

fn {tk : tkind}
spower_portion_degree2
          (@(c0 : g0float tk, c1 : g0float tk, c2 : g0float tk),
           @(t0 : g0float tk, t1 : g0float tk))
    : @(g0float tk, g0float tk, g0float tk) =
  (* Compose spower(c0, c1, c2) with spower(t0, t1). This will map the
     portion [t0,t1] onto [0,1]. (I used Maxima to compute these
     coefficients symbolically.) *)
  let
    val t0_t0 = t0 * t0
    and t0_t1 = t0 * t1
    and t1_t1 = t1 * t1
    and c2p1m0 = c2 + c1 - c0

    val d0 = c0 + (c2p1m0 * t0) - (c1 * t0_t0)
    and d1 = (c1 * t1_t1) - ((c1 + c1) * t0_t1) + (c1 * t0_t0)
    and d2 = c0 + (c2p1m0 * t1) - (c1 * t1_t1)
  in
    @(d0, d1, d2)
  end

fn {a : t@ype} {tk : tkind}
spower_draw_degree2
          {w, h  : int}
          (pix   : !pixmap (a, w, h),
           cx    : @(g0float tk, g0float tk, g0float tk),
           cy    : @(g0float tk, g0float tk, g0float tk),
           color : a)
    : void =
  (* The algorithm is adaptive flattening by repeated bisection. This
     is done in the s-power basis. (The algorithm of de Casteljau is
     NOT used.) *)
  let
    extern castfn sz2lli : {i : int} size_t i -<> llint i
    extern castfn dbl2lli : double -<> [i : int] llint i
    extern castfn f2dbl : g0float tk -<> double

    fn {}
    round_to_lli (x : g0float tk) :<> [i : int] llint i =
      let
        val x = f2dbl x
        val x = $extfcall (double, "rintl", x)
      in
        dbl2lli x
      end

    #define TOL 0.25     (* QUESTION: What is a good value for TOL? *)

    fun
    flatten_and_draw
              {p    : addr}
              (pix  : !pixmap (a, w, h, p),
               iter : bisect_iter)
        : void =
      if ~bisect_iter_done iter then
        let
          val interv = bisect_iter_interval<tk> iter
          val dx = spower_portion_degree2<tk> (cx, interv)
          and dy = spower_portion_degree2<tk> (cy, interv)
        in
          if abs dx.1 < g0f2f TOL
                && abs dy.1 < g0f2f TOL then
            let
              val w = sz2lli (width pix) and h = sz2lli (height pix)
              val dx0 = round_to_lli dx.0
              and dy0 = round_to_lli dy.0
              and dx2 = round_to_lli dx.2
              and dy2 = round_to_lli dy.2
            in
              if (0LL <= dx0) * (dx0 < w)
                    * (0LL <= dy0) * (dy0 < h)
                    * (0LL <= dx2) * (dx2 < w)
                    * (0LL <= dy2) * (dy2 < h) then
                draw_line<a><llintknd>
                  (pix, @(dx0, dy0), @(dx2, dy2), color);
              flatten_and_draw (pix, bisect_iter_next iter)
            end
          else
            flatten_and_draw (pix, bisect_iter_bisect iter)
        end
  in
    flatten_and_draw (pix, bisect_iter_make ())
  end

implement {a} {tk}
pixmap_draw_bezier_degree2_control_points (pix, pt0, pt1, pt2,
                                           color) =
  let
    (* Bernstein coefficients. *)
    val bx = @(pt0.0, pt1.0, pt2.0)
    and by = @(pt0.1, pt1.1, pt2.1)

    (* s-power coefficients. *)
    val cx = bernstein2spower_degree2<tk> bx
    and cy = bernstein2spower_degree2<tk> by
  in
    spower_draw_degree2<a><tk> (pix, cx, cy, color)
  end

(* -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - *)
(* Degree 3. *)

fn {tk : tkind}
bernstein2spower_degree3
          @(c0 : g0float tk, c1 : g0float tk,
            c2 : g0float tk, c3 : g0float tk)
    :<> @(g0float tk, g0float tk, g0float tk, g0float tk) =
  (* Convert from Bernstein coefficients ("control points") to s-power
     coefficients. *)
  let
    (* First convert to scaled Bernstein basis. *)
    val c1 = g0i2f 3 * c1
    and c2 = g0i2f 3 * c2
  in
    (* Then to s-power basis. *)
    @(c0, c1 - c0 - c0 - c3, c2 - c0 - c3 - c3, c3)
  end

fn {tk : tkind}
spower_eval_degree3
          (@(c0 : g0float tk, c1 : g0float tk,
             c2 : g0float tk, c3 : g0float tk),
           t : g0float tk)
    :<> g0float tk =
  (* Evaluate the polynomial spower(c0, c1, c2, c3) at t. (This
     function is used in the test code.) *)
  let
    val one_minus_t = g0i2f 1 - t
    val s = t * one_minus_t
    val y0 = c0 + (c1 * s) and y1 = c3 + (c2 * s)
  in
    (y0 * one_minus_t) + (y1 * t)
  end

fn {tk : tkind}
spower_portion_degree3
          (@(c0 : g0float tk, c1 : g0float tk,
             c2 : g0float tk, c3 : g0float tk),
           @(t0 : g0float tk, t1 : g0float tk))
    : @(g0float tk, g0float tk, g0float tk, g0float tk) =
  (* Compose spower(c0, c1, c2, c3) with spower(t0, t1). This will map
     the portion [t0,t1] onto [0,1]. (I used Maxima to compute these
     coefficients symbolically.) *)
  let
    (* I do not try to find the most efficient Horner scheme. Let us
       simply expand out the products of t0 and t1 that will be
       needed, and try to group them a bit. *)
    val t0_t0 = t0 * t0
    and t0_t1 = t0 * t1
    and t1_t1 = t1 * t1
    val t0_t0_t0 = t0_t0 * t0
    and t0_t0_t1 = t0_t0 * t1
    and t0_t1_t1 = t0 * t1_t1
    and t1_t1_t1 = t1 * t1_t1
    and c3p1m0 = c3 + c1 - c0
    and c2m1m1 = c2 - c1 - c1
    and c1m2 = c1 - c2
    val c2m1 = ~c1m2
    and three_c1m2 = g0i2f 3 * c1m2
    val two_c2m1 = c2m1 + c2m1
    and two_c2m1m1 = c2m1m1 + c2m1m1

    val d0 = c0 + (c3p1m0 * t0) + (c2m1m1 * t0_t0) + (c1m2 * t0_t0_t0)
    and d1 = (c2m1 * t1_t1_t1) + (three_c1m2 * t0_t0_t1)
                + (two_c2m1m1 * t0_t1) + (two_c2m1 * t0_t0_t0)
                - (c2m1m1 * (t0_t0 + t1_t1))
    and d2 = (c2m1 * t0_t0_t0) + (three_c1m2 * t0_t1_t1)
                + (two_c2m1m1 * t0_t1) + (two_c2m1 * t1_t1_t1)
                - (c2m1m1 * (t0_t0 + t1_t1))
    and d3 = c0 + (c3p1m0 * t1) + (c2m1m1 * t1_t1) + (c1m2 * t1_t1_t1)
  in
    @(d0, d1, d2, d3)
  end

fn {tk : tkind}
spower_inflections_degree3
          (@(cx0 : g0float tk, cx1 : g0float tk,
             cx2 : g0float tk, cx3 : g0float tk),
           @(cy0 : g0float tk, cy1 : g0float tk,
             cy2 : g0float tk, cy3 : g0float tk))
    : @(g0float tk, g0float tk) =
  (* Find any value of t at which there is an inflection or cusp. Note
     that such values may lie outside [0,1]. *)
  let
    val nan : g0float tk = g0f2f ($extval (g0float tk, "NAN"))

    (* Translate the curve so its first endpoint be at (0,0). *)
    val cx3m0 = cx3 - cx0
    and cy3m0 = cy3 - cy0

    (* I used Maxima to compute this solution. It is for a curve
       translated so its first endpoint should lie at (0,0). *)

    val cx1cy2 = cx1 * cy2
    and cx2cy1 = cx2 * cy1
    and cx1cy3m0 = cx1 * cy3m0
    and cx2cy3m0 = cx2 * cy3m0
    and cx3m0cy1 = cx3m0 * cy1
    and cx3m0cy2 = cx3m0 * cy2

    val cx1cy2_m_cx2cy1 = cx1cy2 - cx2cy1
    and cx1cy3m0_m_cx2cy3m0 = cx1cy3m0 - cx2cy3m0
    and cx1cy3m0_m_cx3m0cy1 = cx1cy3m0 - cx3m0cy1

    macdef compute_A = g0i2f 3 * cx1cy2_m_cx2cy1
    macdef compute_B =
      g0i2f 3 * (~cx1cy3m0_m_cx2cy3m0 + cx3m0cy1 - cx3m0cy2
                  - cx1cy2_m_cx2cy1)
    macdef compute_C =
      cx1cy3m0_m_cx3m0cy1 + cx1cy3m0_m_cx3m0cy1
        + cx3m0cy2 - cx2cy3m0 + cx1cy2_m_cx2cy1

    val A = compute_A
  in
    if A = g0i2f 0 then
      let
        val B = compute_B
      in
        if B = g0i2f 0 then
          @(nan, nan)
        else
          let
            val C = compute_C
          in
            @(~C / B, nan)
          end
      end
    else
      let
        val B = compute_B and C = compute_C
        val discriminant = (B * B) - (g0i2f 4 * A * C)
      in
        if discriminant < g0i2f 0 then
          @(nan, nan)
        else
          let
            val sqrt_discr = sqrt<tk> discriminant
          in
            @((~B - sqrt_discr) / (A + A),
              (~B + sqrt_discr) / (A + A))
          end
      end
  end

fn {a : t@ype} {tk : tkind}
spower_draw_degree3
          {w, h  : int}
          (pix   : !pixmap (a, w, h),
           cx    : @(g0float tk, g0float tk, g0float tk, g0float tk),
           cy    : @(g0float tk, g0float tk, g0float tk, g0float tk),
           color : a)
    : void =
  (* The algorithm is adaptive flattening by repeated bisection. This
     is done in the s-power basis. (The algorithm of de Casteljau is
     NOT used.) First, though, the curve is broken at its inflection
     points, mainly in case such a point be a cusp. *)
  let
    extern castfn sz2lli : {i : int} size_t i -<> llint i
    extern castfn dbl2lli : double -<> [i : int] llint i
    extern castfn f2dbl : g0float tk -<> double

    fn {}
    round_to_lli (x : g0float tk) :<> [i : int] llint i =
      let
        val x = f2dbl x
        val x = $extfcall (double, "rintl", x)
      in
        dbl2lli x
      end

    #define TOL 0.25     (* QUESTION: What is a good value for TOL? *)

    fun
    flatten_and_draw
              {p    : addr}
              (pix  : !pixmap (a, w, h, p),
               cx   : @(g0float tk, g0float tk,
                        g0float tk, g0float tk),
               cy   : @(g0float tk, g0float tk,
                        g0float tk, g0float tk),
               iter : bisect_iter)
        : void =
      if ~bisect_iter_done iter then
        let
          val interv = bisect_iter_interval<tk> iter
          val dx = spower_portion_degree3<tk> (cx, interv)
          and dy = spower_portion_degree3<tk> (cy, interv)
        in
          if abs dx.1 + abs dx.2 < g0f2f TOL
                && abs dy.1 + abs dy.2 < g0f2f TOL then
            let
              val w = sz2lli (width pix) and h = sz2lli (height pix)
              val dx0 = round_to_lli dx.0
              and dy0 = round_to_lli dy.0
              and dx3 = round_to_lli dx.3
              and dy3 = round_to_lli dy.3
            in
              if (0LL <= dx0) * (dx0 < w)
                    * (0LL <= dy0) * (dy0 < h)
                    * (0LL <= dx3) * (dx3 < w)
                    * (0LL <= dy3) * (dy3 < h) then
                draw_line<a><llintknd>
                  (pix, @(dx0, dy0), @(dx3, dy3), color);
              flatten_and_draw (pix, cx, cy, bisect_iter_next iter)
            end
          else
            flatten_and_draw (pix, cx, cy, bisect_iter_bisect iter)
        end

    val @(t1, t2) = spower_inflections_degree3 (cx, cy)

    macdef zero = g0i2f 0
    macdef one = g0i2f 1
  in
    if zero <= t1 && t1 <= one then
      begin
        if t1 = t2 then
          let
            val cx_0 = spower_portion_degree3<tk> (cx, @(zero, t1))
            and cy_0 = spower_portion_degree3<tk> (cy, @(zero, t1))
            and cx_1 = spower_portion_degree3<tk> (cx, @(t1, one))
            and cy_1 = spower_portion_degree3<tk> (cy, @(t1, one))
          in
            flatten_and_draw (pix, cx_0, cy_0, bisect_iter_make ());
            flatten_and_draw (pix, cx_1, cy_1, bisect_iter_make ())
          end
        else if zero <= t2 && t2 <= one then
          let
            val t1 = min (t1, t2)
            and t2 = max (t1, t2)
            val cx_0 = spower_portion_degree3<tk> (cx, @(zero, t1))
            and cy_0 = spower_portion_degree3<tk> (cy, @(zero, t1))
            and cx_1 = spower_portion_degree3<tk> (cx, @(t1, t2))
            and cy_1 = spower_portion_degree3<tk> (cy, @(t1, t2))
            and cx_2 = spower_portion_degree3<tk> (cx, @(t2, one))
            and cy_2 = spower_portion_degree3<tk> (cy, @(t2, one))
          in
            flatten_and_draw (pix, cx_0, cy_0, bisect_iter_make ());
            flatten_and_draw (pix, cx_1, cy_1, bisect_iter_make ());
            flatten_and_draw (pix, cx_2, cy_2, bisect_iter_make ())
          end
      end
    else if zero <= t2 && t2 <= one then
      let
        val cx_0 = spower_portion_degree3<tk> (cx, @(zero, t2))
        and cy_0 = spower_portion_degree3<tk> (cy, @(zero, t2))
        and cx_1 = spower_portion_degree3<tk> (cx, @(t2, one))
        and cy_1 = spower_portion_degree3<tk> (cy, @(t2, one))
      in
        flatten_and_draw (pix, cx_0, cy_0, bisect_iter_make ());
        flatten_and_draw (pix, cx_1, cy_1, bisect_iter_make ())
      end
    else
      flatten_and_draw (pix, cx, cy, bisect_iter_make ())
  end

implement {a} {tk}
pixmap_draw_bezier_degree3_control_points (pix, pt0, pt1, pt2, pt3,
                                           color) =
  let
    (* Bernstein coefficients. *)
    val bx = @(pt0.0, pt1.0, pt2.0, pt3.0)
    and by = @(pt0.1, pt1.1, pt2.1, pt3.1)

    (* s-power coefficients. *)
    val cx = bernstein2spower_degree3<tk> bx
    and cy = bernstein2spower_degree3<tk> by
  in
    spower_draw_degree3<a><tk> (pix, cx, cy, color)
  end

(*------------------------------------------------------------------*)

#ifdef BITMAP_BRESENHAM_TASKS_TEST #then

staload "bitmap_write_ppm_task.sats"
staload _ = "bitmap_write_ppm_task.dats"

fn
draw_some_lines () : void =
  let
    val @(pfgc | pix) =
      pixmap_make_elt<rgb24> (i2sz 200, i2sz 100,
                              rgb24_make (217, 217, 214))
    val () = draw_line (pix, @(0, 0), @(199, 99),
                        rgb24_make (39, 93, 56))
    val () = draw_line (pix, @(0, 99), @(199, 0),
                        rgb24_make (0, 51, 160))
    val () = draw_line (pix, @(90, 0), @(110, 99),
                        rgb24_make (125, 85, 199))
    val () = draw_line (pix, @(110, 0), @(90, 99),
                        rgb24_make (137, 60, 71))
    val () = draw_line (pix, @(199, 89), @(0, 10),
                        rgb24_make (0, 134, 191))
    val () = draw_line (pix, @(199, 20), @(0, 79),
                        rgb24_make (89, 203, 232))
    val () = draw_line (pix, @(99, 79), @(119, 30),
                        rgb24_make (235, 111, 189))
    val () = draw_line (pix, @(99, 79), @(79, 30),
                        rgb24_make (179, 155, 0))
    val () = draw_line (pix, @(0, 10), @(199, 10),
                        rgb24_make (200, 130, 66))
    val () = draw_line (pix, @(199, 90), @(0, 90),
                        rgb24_make (187, 166, 0))
    val () = draw_line (pix, @(10, 0), @(10, 99),
                        rgb24_make (0, 119, 200))
    val () = draw_line (pix, @(190, 99), @(190, 0),
                        rgb24_make (92, 78, 99))
    val () = draw_line (pix, @(1199, ~10), @(~1000, 109),
                        rgb24_make (162, 0, 103))
    val () = draw_line (pix, @(1199, 109), @(~1000, ~10),
                        rgb24_make (244, 54, 76))
    val outf = fileref_open_exn ("some_lines.ppm", file_mode_w)
  in
    ignoret (pixmap_write_ppm<rgb24> (outf, pix));
    fileref_close outf;
    free (pfgc | pix)
  end

fn
draw_some_circles () : void =
  let
    val @(pfgc | pix) =
      pixmap_make_elt<rgb24> (i2sz 200, i2sz 100,
                              rgb24_make (215, 210, 203))
    val () = draw_circle (pix, @(99, 49), 30, rgb24_make(98, 52, 18))
    val () = draw_circle (pix, @(0, 0), 99, rgb24_make(34, 55, 43))
    val () = draw_circle (pix, @(160, 55), 50, rgb24_make(0, 85, 140))
    val outf = fileref_open_exn ("some_circles.ppm", file_mode_w)
  in
    ignoret (pixmap_write_ppm<rgb24> (outf, pix));
    fileref_close outf;
    free (pfgc | pix)
  end

fn
test_portion_degree2 () : void =
  let
    val b = @(3.0, ~5.0, 17.0)
    val c = bernstein2spower_degree2<dblknd> b
    
    val t0 = 0.2 and t1 = 0.7
    val d = spower_portion_degree2<dblknd> (c, @(t0, t1))

    var t : double
  in
    for (t := 0.0; t <= 1.0; t := t + 0.025)
      let
        val f = spower_eval_degree2<dblknd> (c, t0 + ((t1 - t0) * t))
        val g = spower_eval_degree2<dblknd> (d, t)
        val- true = abs (f - g) < 0.00000001
      in
      end
  end

fn
test_portion_degree3 () : void =
  let
    val b = @(3.0, ~5.0, 17.0, ~70.0)
    val c = bernstein2spower_degree3<dblknd> b
    
    val t0 = 0.2 and t1 = 0.7
    val d = spower_portion_degree3<dblknd> (c, @(t0, t1))

    var t : double
  in
    for (t := 0.0; t <= 1.0; t := t + 0.025)
      let
        val f = spower_eval_degree3<dblknd> (c, t0 + ((t1 - t0) * t))
        val g = spower_eval_degree3<dblknd> (d, t)
        val- true = abs (f - g) < 0.00000001
      in
      end
  end

fn
test_inflections () : void =
  begin
    let
      (* Some of the following test examples are from the paper
         referenced at https://archive.ph/1mUxT *)

      (* This should have a cusp at t = 0.5. *)
      val bx = @(0.0, 1.0, 0.0, 1.0)
      and by = @(0.0, 1.0, 1.0, 0.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.5) < 0.00000001
      val- true = abs (t2 - 0.5) < 0.00000001
    in
    end;
    let
      (* This should have an inflection at t = 0.5 *)
      val bx = @(0.0, 1.0, 2.0, 3.0)
      and by = @(1.0, 1.0, ~1.0, ~1.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.5) < 0.00000001
    in
    end;
    let
      (* This should have one inflection point in [0,1]. *) 
      val bx = @(16.0, 185.0, 673.0, 810.0)
      and by = @(467.0, 95.0, 545.0, 17.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.456590) < 0.00001
      val- true = abs (t2 - ~24.047383) < 0.00001
    in
    end;
    let
      (* This should have two inflections in [0,1]. *)
      val bx = @(859.0, 13.0, 781.0, 266.0)
      and by = @(676.0, 422.0, 12.0, 425.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.681076) < 0.00001
      val- true = abs (t2 - 0.705299) < 0.00001
    in
    end;
    let
      (* This should have two inflections in [0,1]. *)
      val bx = @(872.0, 11.0, 779.0, 220.0)
      and by = @(686.0, 423.0, 13.0, 376.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.588071) < 0.00001
      val- true = abs (t2 - 0.886863) < 0.00001
    in
    end;
    let
      (* This should have two inflections in [0,1]. *)
      val bx = @(819.0, 43.0, 826.0, 25.0)
      and by = @(566.0, 18.0, 18.0, 533.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.476169) < 0.00001
      val- true = abs (t2 - 0.539295) < 0.00001
    in
    end;
    let
      (* This should have two inflections in [0,1]. *)
      val bx = @(819.0, 43.0, 826.0, 25.0)
      and by = @(566.0, 18.0, 18.0, 533.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.476169) < 0.00001
      val- true = abs (t2 - 0.539295) < 0.00001
    in
    end;
    let
      (* This should have two inflections in [0,1]. *)
      val bx = @(884.0, 135.0, 678.0, 14.0)
      and by = @(574.0, 14.0, 14.0, 566.0)
      val cx = bernstein2spower_degree3<dblknd> bx
      and cy = bernstein2spower_degree3<dblknd> by
      val @(t1, t2) = spower_inflections_degree3<dblknd> (cx, cy)
      val- true = abs (t1 - 0.320836) < 0.00001
      val- true = abs (t2 - 0.682291) < 0.00001
    in
    end;
  end

fn
draw_some_quadratics () : void =
  let
    val @(pfgc | pix) =
      pixmap_make_elt<rgb24> (i2sz 200, i2sz 100,
                              rgb24_make (177, 228, 227))
    val () = draw_bezier (pix, @(0.0, 49.0), @(0.0, 99.0),
                          @(99.0, 99.0), rgb24_make (166, 9, 61))
    val () = draw_bezier (pix, @(99.0, 99.0), @(199.0, 99.0),
                          @(199.0, 49.0), rgb24_make (234, 39, 194))
    val () = draw_bezier (pix, @(199.0, 49.0), @(199.0, 0.0),
                          @(99.0, 0.0), rgb24_make (113, 92, 42))
    val () = draw_bezier (pix, @(99.0, 0.0), @(0.0, 0.0),
                          @(0.0, 49.0), rgb24_make (0, 83, 76))
    val outf = fileref_open_exn ("some_quadratics.ppm", file_mode_w)
  in
    ignoret (pixmap_write_ppm<rgb24> (outf, pix));
    fileref_close outf;
    free (pfgc | pix)
  end

fn
draw_some_cubics () : void =
  let
    val @(pfgc | pix) =
      pixmap_make_elt<rgb24> (i2sz 200, i2sz 100,
                              rgb24_make (197, 207, 218))

    (* This has a cusp. *)
    val () = draw_bezier (pix, @(0.0, 0.0), @(99.0, 99.0),
                          @(0.0, 99.0), @(99.0, 0.0),
                          rgb24_make (166, 9, 61))

    (* This self-intersects. *)
    val () = draw_bezier (pix, @(100.0, 0.0), @(249.0, 99.0),
                          @(50.0, 99.0), @(199.0, 0.0),
                          rgb24_make (0, 81, 81))

    (* This is a closed curve. *)
    val () = draw_bezier (pix, @(50.0, 50.0), @(250.0, 20.0),
                          @(0.0, 90.0), @(50.0, 50.0),
                          rgb24_make (0, 83, 76))

    val () = draw_bezier (pix, @(0.0, 0.0), @(49.0, 99.0),
                          @(149.0, 0.0), @(199.0, 99.0),
                          rgb24_make (113, 92, 42))

    val () = draw_bezier (pix, @(199.0, 0.0), @(0.0, 0.0),
                          @(250.0, 200.0), @(0.0, 0.0),
                          rgb24_make (111, 44, 63))

    (* This one is only partly inside the window. *)
    val () = draw_bezier (pix, @(~10.0, 0.0), @(110.0, 0.0),
                          @(110.0, 99.0), @(~10.0, 99.0),
                          rgb24_make (111, 44, 63))

    val outf = fileref_open_exn ("some_cubics.ppm", file_mode_w)
  in
    ignoret (pixmap_write_ppm<rgb24> (outf, pix));
    fileref_close outf;
    free (pfgc | pix)
  end

implement
main0 () =
  begin
    draw_some_lines ();
    draw_some_circles ();
    test_portion_degree2 ();
    test_portion_degree3 ();
    test_inflections ();
    draw_some_quadratics ();
    draw_some_cubics ()
  end

#endif

(*------------------------------------------------------------------*)
Output:

To compile and run the test program:

$ patscc -std=gnu2x -g -O2 -DATS_MEMALLOC_LIBC -DATS BITMAP_BRESENHAM_TASKS_TEST bitmap_{bresenham_tasks,{,write_ppm_}task}.{s,d}ats -lm
$ ./a.out

The program should create Portable Pixel Map files some_lines.ppm, some_circles.ppm, some_quadratics.ppm, and some_cubics.ppm:

Straight lines in multiple colors, drawn by Bresenham's algorithm A circle and parts of circles, drawn as outlines in multiple colors. An oval made of four quadratic splines, each spline in a different color, with an aqua background. Cubic splines in multiple colors. There is a variety of cusps, inflections, self-intersections, closedness, and sometimes not the whole spline fits in the window.

A Maxima script I used

It might be useful if I posted the Maxima script I used to help me devise some of the Bézier curve code. The Maxima commands are not organized well, nor documented well, but nevertheless they might prove useful:

/* sbern <--> spow */
T1i : matrix(
  [1, 0],
  [0, 1]);
T1  : invert(T1i);
T2i : matrix(
  [1, 0, 0],
  [1, 1, 1],
  [0, 0, 1]);
T2  : invert(T2i);
T3i : matrix(
  [1, 0, 0, 0],
  [2, 1, 0, 1],
  [1, 0, 1, 2],
  [0, 0, 0, 1]);
T3  : invert(T3i);
expand(T1.matrix([c0],[c1]));
expand(T2.matrix([c0],[c1],[c2]));
expand(T3.matrix([c0],[c1],[c2],[c3]));

/* sbern <--> mono */
S1i : transpose(matrix(
    [1, -1],
    [0, 1]));
S1  : invert(S1i);
S2i : transpose(matrix(
    [1, -2, 1],
    [0, 1, -1],
    [0, 0, 1]));
S2  : invert(S2i);
S3i : transpose(matrix(
    [1, -3, 3, -1],
    [0, 1, -2, 1],
    [0, 0, 1, -1],
    [0, 0, 0, 1]));
S3  : invert(S3i);
F1 : expand(S1i.T1i.matrix([c0],[c1]));
F2 : expand(S2i.T2i.matrix([c0],[c1],[c2]));
F3 : expand(S3i.T3i.matrix([c0],[c1],[c2],[c3]));

/* portion t0..t1 */
g(x) := t0 + ((t1 - t0) * x);
f2(x) := expand(F2.matrix([1],[x],[x**2]));
expand(T2.S2.matrix(
    [coeff(f2(g(t)), t, 0)],
    [coeff(f2(g(t)), t, 1)],
    [coeff(f2(g(t)), t, 2)]));
f3(x) := expand(F3.matrix([1],[x],[x**2],[x**3]));
Solution3 : expand(T3.S3.matrix(
    [coeff(f3(g(t)), t, 0)],
    [coeff(f3(g(t)), t, 1)],
    [coeff(f3(g(t)), t, 2)],
    [coeff(f3(g(t)), t, 3)]));
Solution3[1];
Solution3[2];
Solution3[3];
Solution3[4];

/* derivatives and critical points */
/* In s-power format: */
Mx : S3i.T3i.matrix([cx0],[cx1],[cx2],[cx3]);
My : S3i.T3i.matrix([cy0],[cy1],[cy2],[cy3]);
fx(t) := expand(transpose(Mx).matrix([1],[t],[t**2],[t**3]));
fy(t) := expand(transpose(My).matrix([1],[t],[t**2],[t**3]));
fxp(t) := diff(fx(t), t);
fxpp(t) := diff(fxp(t), t);
fyp(t) := diff(fy(t), t);
fypp(t) := diff(fyp(t), t);
cross(t) := expand((fxp(t) * fypp(t)) - (fyp(t) * fxpp(t)));
expand(fxp(t));
expand(fyp(t));
expand(cross(t) / 2); /* Notice that this is quadratic. */
/* Translating the first endpoint to (0,0), and the second to
   cx3 - cx0. */
Mx : S3i.T3i.matrix([0],[cx1],[cx2],[cx3m0]);
My : S3i.T3i.matrix([0],[cy1],[cy2],[cy3m0]);
fx(t) := expand(transpose(Mx).matrix([1],[t],[t**2],[t**3]));
fy(t) := expand(transpose(My).matrix([1],[t],[t**2],[t**3]));
fxp(t) := diff(fx(t), t);
fxpp(t) := diff(fxp(t), t);
fyp(t) := diff(fy(t), t);
fypp(t) := diff(fyp(t), t);
cross(t) := expand((fxp(t) * fypp(t)) - (fyp(t) * fxpp(t)));
expand(fxp(t));
expand(fyp(t));
expand(cross(t) / 2); /* Notice that this is quadratic. */

Some of the calculations are done by assigning names to the coefficients of splines in the s-power basis, finding the corresponding coefficients in the ordinary monomial basis, then (using Maxima's inherent capabilities) performing an operation such as composition or differentiation. It is a sloppy script.