Electric Field Simulator and Art Creator

I’m excited to announce my electric field simulator and “electric field art” creator. This project started when I learned about the electric field in my Physics class and wanted to simulate it in Elm. The results are totally beyond my conception of electric fields from the textbook. Here are some pieces I made with this tool:

Try it out yourself here (Recommend opening in Chrome)

What I learned

Physics + CS + Art = Magic!

Electric fields are based on a very simple idea: like charges repel and opposite charges attract. However, we can form countless interesting lines and shapes by adjusting the position, strength, and sign of the charges. Because these shapes follow the law of electricity, they form peculiar symmetries, curves, and layers. Maybe this is just the magic of nature. If you are interested in digital art and how to create art using Elm, definitely check out Noah Z Gordon’s Generative Art in Elm. It’s a great inspiration for me and one of the best demonstrations of the power of Elm in making art.

It’s possible to go without JS

I probably don’t need to explain why I try my best to avoid JS. On most of my previous Elm projects, I usually resort to JS when doing stuff that is more involved with the browser API like getting the window width and handling touch events. This time, however, I looked harder and found many useful Elm implementations for complicated tasks like dragging and tapping (elm-draggable) and handling context menu (elm-pointer-events).

It’s possible to go without CSS

Compared to JS, I had a better experience with CSS. However, I still remembered me spending hours figuring out how to precisely select the element I want and how cascading and inheritance can mess it all up. Again, awesome Elm solutions like elm-ui and typed-svg saved my day.

By using elm-ui, I can

  • Layout all components in intuitive ways through row, column, and alignment attributes
  • Style individual elements locally in the Elm view code with just the right amount of style inheritance
  • Extract common styles into an Elm record for reuse
  • Show and hide elements according to model state
  • Opt into HTML and CSS with html and htmlAttribute when interacting with Svgs and more complicated attributes and events

By using typed-svg, I can

It’s possible to optimize speed for free

The electric field simulation is fairly intense in calculations. Each point on the field line requires summing up the electric field vector from all charges. One whole line requires running the Euler’s method hundreds of times to get a smooth curve. The initial load time of simulation is ok but there are long lags when dragging or scaling charges. There are probably some clever ways to optimize the calculations for electric fields to reduce those lags. However, I applied a simple principle used by many 3D design tools: lower display quality when moving and restore back to the original quality when stopped. I essentially decreased the accuracy of the field lines when the user is dragging or scaling the charges and restored back to original accuracy when the user released their control.

Future Plans

  1. Store the current piece in local storage, load and save pieces
  2. Support more style configurations
  • field line thickness
  • field line color
  • charge color
  1. Support line and shape charges
  2. 3D electric field?
  3. Animation of charge position, magnitude, and sign?

Questions

  1. :star: I’m a beginner to electricity so I still can’t think of a way to implement more complex shaped charges like lines, circles, and polygons. If anyone has an idea please help me!
  2. I tested this on Chrome, Firefox, and Edge and discovered that dragging and other controls are only workable on Chrome as they are too slow on Firefox and Edge. Does anyone have some ideas as to why this happens? Is it because Firefox and Edge don’t optimize for SVG graphics enough?
  3. This simulator shows a blank screen on android but seems to work on iOS. Does anyone know why and how to fix?

Link to simulator: https://alienkevin.github.io/electric-fields/
Link to repo: https://github.com/AlienKevin/electric-fields

Updates

4/4/2020:

  • Further speed boost using the principle “out of sight, out of mind”. Now dragging is fast enough on Firefox. Basically, I stopped calculating the field lines when they reach out of the display. Sometimes field lines do curl back in but the missing parts are usually not noticeable.
  • You can download the model as an SVG file!
  • Autosave model to localStorage before page close!
24 Likes

Seems very cool. Also the settings dialog doesn’t display properly for me:

1 Like

Is it Safari? It is hard to develop for Safari unless you buy a Mac.

2 Likes

Thanks for reporting that. I think there may be some issues with elm-ui and safari. I don’t have any apple device so like @francescortiz said it’s a bit of hard to find out exactly why.

4/5/2020:

The simulator now supports multiple tabs and autosaving all tabs before exit!
Also, the newest version supports saving the simulation as a JSON file. You now export your simulation to other devices and share your simulation for others to edit.

Here’s a demo on one way to make cherry blossoms using the simulator:
cherry-blossoms-demo

Try out the new version here: https://alienkevin.github.io/electric-fields/

Very cool!

A couple of suggestions:

  • When a charge is selected, display its value.
  • Scrolling to change charge doesn’t work well for me (Chrome, MacOS). It often scrolls the entire window rather than changing the charge.
1 Like

:pray: Thanks for trying out! I will add an option to display the charge value in the settings. As for scrolling, the problem you mentioned is because I didn’t do responsive design. It works fine as long as all contents fit the screen (ie. I only tested on my 15-inch laptop). Having other users like you really helps me identify blind spots :blush:. I will add the responsive design.

By default, html pages allow overscroll on MacOS. Setting overflow: hidden on the html element should fix the bug.

I’ve also run into the page freezing / becoming unresponsive several times. I’m not sure what to recommend there, but using a lower quality render during scrolling the same way you do for moving the charges might help.

The freezing during scrolling puzzles me because I applied the same optimization as dragging. Chrome on my Windows 10 seems to handle is very good but firefox and edge still freeze. I think it may because of the frequent timeOut I set up to detect the start and end of scrolling because the browser doesn’t provide this service. Do you have thoughts on improving that?

The logic for that doesn’t look quite right to me. It appears that we optimize the model in scaleSourceMagnitude and deoptimize it in stopWheelingTimeOut. We’ll get one call to stopWheelingTimeOut for each call to scaleSourceMagnitude, and we will generally get several calls to scaleSourceMagnitude during each scroll. It looks like we will deoptimize the model while scrolling if we scroll for more than 200ms and we get two calls to stopWheelingTimeOut without a call to scaleSourceMagnitude in between.

The simplest fix is probably to turn isWheelingTimeOutCleared into a count rather than a boolean.

This is just fantastic!!!

I think Safari has implemented SVG in its own Appleish way. I have some projects where SVG is displayed wrong (upside down if I recall correctly). So I just don’t use it. Hope that they will get with the program one of these days.

I think you may overlook that I clear the timeout every 200ms, if the wheeling is scrolling, using the boolean variable isWheelingTimeOutCleared.
BTW, I published the next version that displays charge value by default and accommodates different screen sizes. While scaling charge magnitude is smoother because I restricted the scaling steps to 0.5, I still experience sudden froze on firefox.

Right, if it tries to clear the timeout twice, it will deoptimize the model.

Very awesome! Reminded me an idea I haven’t implemented of designing a “The Incredible Machine” style puzzle game but with electricity and magnetism physics.

Hi, after some fiddling I managed to do some serious optimization by porting calculation to web assembly compiled from Rust. Here’s the wasm version. Oh god, the speed is at least doubled. However, I experience some weird quirks while dragging and local storage stopped working. Maybe it’s because of the service worker I used for a progressive web app? Or is there any race condition between subscriptions and commands back and force between elm, js, and rust? I have trouble locating the issue. Can someone help?

repo: https://github.com/AlienKevin/electric-fields-with-rust

BTW: Now we support simulating electric forces as well.

Here’s a very crude visual speed comparison:
Pure Elm:
19625d2979da574c13b753e1649b0ef8

Elm with WASM compiled from Rust:
9f95c3030a7395a5b97ef472a7341f36

1 Like

How to optimize Elm with Rust?

Say we are trying to optimize the calculateFields function which calculates the positions of all field lines of a charge.

In Elm you may have the function defined as:

calculateFields : Float -> Float -> List Field -> List Field
calculateFields width height fields =
   ...

We will go through the basic logic of Interoperation between Elm and Rust. The code I show is just for demonstration and is incomplete for the sake of simplicity.

First step, create encode and decoder for all non-primitive types in the function type declaration (parameter and return types).

This process is very mechanical, you just map each field of a record to their encoder/decoder.

  • Encoding Fields:
encodeFields : List Field -> Encode.Value
encodeFields fields =
  Encode.list encodeField fields

encodeField : Field -> Encode.Value
encodeField { source, density, steps, delta } =
  Encode.object
  [ ("source", encodeCharge source)
  , ("density", Encode.int density)
  , ("steps", Encode.int steps)
  , ("delta", Encode.float delta)
  ]
  • Decoding Fields:
decodeFields : Decoder (List Field)
decodeFields =
  Decode.list decodeField

decodeField : Decoder Field
decodeField =
  Field.require "source" decodeCharge <| \source ->
  Field.require "density" Decode.int <| \density ->
  Field.require "steps" Decode.int <| \steps ->
  Field.require "delta" Decode.float <| \delta ->
  Field.attempt "lines" (Decode.list (Decode.list decodePoint)) <| \lines ->

  Decode.succeed
    { source = source
    , density = density
    , steps = steps
    , delta = delta
    , lines = Maybe.withDefault [] lines
    }

Second step, replace concrete Elm implementation with a port.

port calculateFieldsPort : (Float, Float, Encode.Value) -> Cmd msg

Third step, listen to the port’s return value through a subscription.

port receiveFieldsPort : (Encode.Value -> msg) -> Sub msg 
subscriptions : Model -> Sub Msg
subscriptions _ =
    receiveFieldsPort ReceivedFields

Fourth step, subscribe to Elm port in your JS code.

Notice the call to a wasm.calculate_fields. That will be our Rust function.

app.ports.calculateFieldsPort.subscribe(function([width, height, fields_in_json]) {
  app.ports.receiveFieldsPort.send(wasm.calculate_fields(width, height, fields_in_json));
});

Fifth step, define equivalent parameter types in Rust.

What’s great about Rust is that a serialization/deserialization library called Serde provides a default implementation of encoder/decoder for any type. The default is good enough for our case.

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Field {
  source: Charge,
  density: usize,
  steps: usize,
  delta: f64,
  #[serde(skip_deserializing)]
  lines: Vec<Line>,
}

Sixth step, define equivalent function in Rust.

Notice the JsValue type? You can pass in any JS types including strings, objects, arrays, and so on to Rust and the package wasm-bindgen will handle any wrapping and conversion for you.

#[wasm_bindgen]
pub fn calculate_fields( width: f64, height: f64, fields_in_json: &JsValue ) -> JsValue

If you want to learn more about how to power Elm with Rust, check out my template for creating a progressive web app using Elm and Rust. You may also want to check out Rust’s wasm-bindgen guide and more specifically how to pass arbitrary data between JS and Rust

2 Likes

How to rewrite Elm code in Rust for performance?

For those curious about the difference between Rust and Elm and how hard it is to rewrite performance-critical parts in Rust, my experience is that translation is actually very straight-forward and definitoin-by-definition. Like Elm, Rust is expression-oriented, uses combinators like fold and map, uses sum and product types, etc. The hardest part is to find similar libraries in Rust but everything afterward is much easier.

Here’s a comparison of the Field type in Elm and Rust:

Elm

type alias Field =
  { source: Charge
  , density: Int
  , steps: Int
  , delta: Float
  , lines: List Line
  }

Rust

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Field {
  source: Charge,
  density: usize,
  steps: usize,
  delta: f64,
  #[serde(skip_deserializing)]
  lines: Vec<Line>,
}

Here’s a comparison of the core calculation function in Elm and Rust:

Elm (73 lines)

calculateFieldLine :
  { charges : List Charge
  , steps : Int
  , delta : Float
  , sourceSign : Sign
  , start : Point
  , xBound : Float
  , yBound : Float
  } -> Line
calculateFieldLine { charges, steps, delta, sourceSign, start, xBound, yBound } =
  foldlWhile
    (\_ line ->
      let
        (x, y) =
          case line of
            prev :: _ ->
              prev
            _ ->
              (0, 0) -- impossible
        previousPosition =
          vec2 x y
        outOfBounds =
          x > xBound || x < 0 || y > yBound || y < 0
        netField =
          if outOfBounds then
            Vector2.vec2 0 0
          else
            List.foldl
            (\charge sum ->
              let
                d =
                  Vector2.distance previousPosition charge.position / 100
                magnitude =
                  charge.magnitude / (d ^ 2)
                sign =
                  case charge.sign of
                    Positive ->
                      1
                    Negative ->
                      -1
                field =
                  Vector2.scale (sign * magnitude) <|
                    Vector2.normalize <|
                      Vector2.sub previousPosition charge.position
              in
              Vector2.add sum field
            )
            (Vector2.vec2 0 0)
            charges
        next =
          if outOfBounds then
            (x, y)
          else
            let
              vec =
                Vector2.add
                  (Vector2.vec2 x y)
                  ((case sourceSign of
                    Positive ->
                      identity
                    Negative ->
                      Vector2.negate
                  )<|
                    Vector2.scale delta <|
                      Vector2.normalize netField
                  )
            in
            (Vector2.getX vec, Vector2.getY vec)
      in
      (next :: line, outOfBounds)
  )
  [ start ]
  (List.range 0 (steps - 1))

Rust (51 lines)

fn calculate_field_line(charges: &Vec<Charge>, steps: usize, delta: f64, source_sign: Sign, start: Point, x_bound: f64, y_bound: f64) -> Line {
  (0..steps - 1).fold_while(vec![ start ], |mut line: Line, _| {
    let [x, y] = match line {
      _ if line.len() > 0 =>
        line[line.len()-1],
      _ =>
        [0.0, 0.0] // impossible
    };
    let previous_position: Vector2 = [x, y];
    let tolerance = 100.0;
    let out_of_bounds = x > x_bound + tolerance || x < -tolerance || y > y_bound + tolerance || y < -tolerance;
    if out_of_bounds {
      Done(line)
    } else {
      let net_field =
        charges.iter().fold([0.0, 0.0], |sum, charge| {
          let charge_position = position_to_vector2(&charge.position);
          let d = distance(previous_position, charge_position) / 100.0;
          let magnitude = charge.magnitude / d.powf(2.0);
          let sign =
            match charge.sign {
              Sign::Positive => 1.0,
              Sign::Negative => -1.0
            };
          let field =
            vecmath::vec2_scale(
              vecmath::vec2_normalized(
                vecmath::vec2_sub(previous_position, charge_position)
            ), sign * magnitude);
          vecmath::vec2_add(sum, field)
        });
      let delta_vector =
        vecmath::vec2_scale(
          vecmath::vec2_normalized(net_field),
          delta
        );
      let next =
        vecmath::vec2_add(
          previous_position,
          match source_sign {
            Sign::Positive =>
              delta_vector,
            Sign::Negative =>
              vecmath::vec2_neg(delta_vector),
          }
        );
      line.push(next);
      Continue(line)
    }
  }).into_inner()
}
1 Like

I started using this very recently to isolate Safari/WebKit bugs on Linux. It seems to be accurate so far. https://wiki.gnome.org/Apps/Web

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.