Lately I’ve posted one or two examples of very simple Dart web apps that draw graphics on an HTML canvas. Today’s example is another such canvas web app, but this time I’m also demonstrating how your Dart apps can interact with HTML elements–specifically controls on a web page. Behold the Stellarator, an app that draws stars. You can change the number of arms of the star, as well as their inner- and outer-radii. Here’s a screenshot.

Stellarator Screenshot

Click here to run the app yourself!

HTML

Since the app interacts with HTML controls, the HTML is a little more important this time around. Here’s index.html:

<!DOCTYPE html>

<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="scaffolded-by" content="https://github.com/google/stagehand">
    <title>stellarator</title>
    <link rel="stylesheet" href="styles.css">
    <script defer src="main.dart.js"></script>
</head>

<body>

    <div id="canvas_container" class="container">
        <p><a href="https://www.dartographer.com">Dartographer</a> © 2018 Aaron Barrett</p>
        <canvas id="canvas" width="100" height="100"></canvas>
    </div>

    <div class="container">
        <label>Arms</label>
        <input id="arms_slider" type="range" min="3" max="12" value="5">
    </div>

    <div class="container">
        <label>Inner Radius</label>
        <input id="inner_radius_slider" type="range" min="0.05" max="1.00" step="0.05" value="0.25">
    </div>

    <div class="container">
        <label>Outer Radius</label>
        <input id="outer_radius_slider" type="range" min="0.05" max="1.00" step="0.05" value="1.00">
    </div>

</body>
</html>

Notice how I’ve added several "range" sliders. I’m providing default values and minimum/maximum ranges right here in the HTML.

Listening to event streams in Dart

The main.dart file looks pretty familiar.

import "dart:html";

import "canvas_tools.dart";
import "star_builder.dart";

void main() {
  var canvas = CanvasTools.configureSquareCanvas();
  var context = canvas.getContext("2d") as CanvasRenderingContext2D;

  new StarBuilder(canvas, context);
}

As usual, we’re extracting a canvas with CanvasTools, whose code hasn’t changed. Then we immediately pass it into the StarBuilder.

Here’s star_builder.dart.

import "dart:html";
import "dart:math";

class StarBuilder {
  CanvasElement canvas;
  CanvasRenderingContext2D context;

  InputElement armsSlider = querySelector("#arms_slider");
  InputElement innerRadiusSlider = querySelector("#inner_radius_slider");
  InputElement outerRadiusSlider = querySelector("#outer_radius_slider");

  double canvasSize;
  double centerX;
  double centerY;

  int armsCount;
  double innerRadius;
  double outerRadius;

  StarBuilder(this.canvas, this.context) {
    canvasSize = canvas.width.toDouble();
    centerX = canvasSize * 0.5;
    centerY = canvasSize * 0.5;

    armsSlider.onChange.listen((_) => drawStar());
    innerRadiusSlider.onChange.listen((_) => drawStar());
    outerRadiusSlider.onChange.listen((_) => drawStar());

    drawStar();
  }

  void drawStar() {
    updateStarAttributes();

    context
      ..clearRect(0, 0, canvasSize, canvasSize)
      ..setFillColorRgb(255, 228, 0)
      ..setStrokeColorRgb(173, 72, 0)
      ..beginPath()
      ..moveTo(centerX + innerRadius, centerY);

    for (var armIndex = 0; armIndex < armsCount; armIndex++) {
      var angleToOuterPoint = ((armIndex + 0.5) / armsCount) * pi * 2.0;
      var angleToInnerPoint = ((armIndex + 1.0) / armsCount) * pi * 2.0;

      context
        ..lineTo(centerX + cos(angleToOuterPoint) * outerRadius,
            centerY + sin(angleToOuterPoint) * outerRadius)
        ..lineTo(centerX + cos(angleToInnerPoint) * innerRadius,
            centerY + sin(angleToInnerPoint) * innerRadius);
    }

    context
      ..fill()
      ..stroke();
  }

  void updateStarAttributes() {
    armsCount = armsSlider.valueAsNumber;
    innerRadius = innerRadiusSlider.valueAsNumber * 0.5 * canvasSize;
    outerRadius = outerRadiusSlider.valueAsNumber * 0.5 * canvasSize;
  }
}

To make my Dart app respond to HTML controls, first I have to get a handle to those controls from the web page. In Dart, this is simple. I just call querySelector with the id of the element I want to extract.

The other important step is setting up listeners to handle events coming from the web page. In Dart, the three sliders are each instances of InputElement. HTML Elements can generate streams of events. In this case, I’m listening to the sliders’ onChange property, which is a stream that generates events each time the slider’s value changes. I’m passing in a closure, (_) => drawStar(), which will be called every time a change event occurs. I’m not using the generated Event object itself, so I’m using an underscore (_), which indicates that the argument should be discarded. _ is more than a mere convention in Dart–it’s a language-level feature that silences "local variable is not used" warnings when running dartanalyzer.

That was easy

That’s really all the code there is for this example. You can access the code for yourself on GitHub. As always, please feel free to share and modify it!