DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

3 2

Open Source Adventures: Episode 19: Porting Imba 1 Apps to Imba 2

Back in the days when I was into Imba 1, I wrote a lot of toy Imba 1 apps. It's a good time to rewrite them in Imba 2.

There are some difficulties with porting:

  • there's no automated way to convert Imba 1 to Imba 2 code, it's a different dialect of CoffeeScript, and it's not just syntactic
  • the component model is not the same, Imba 2 is based on web components
  • as Imba 2 is based on web components, an overall questionable decision - one huge problem with it is that they just plain won't work with SVG, and I used a lot of SVG in my apps; I don't know if there are any workaround for that Imba could do
  • my mini apps all used SCSS, and Imba 2 has its own Tailwind-like CSS system instead

I did one such port before - Imba 1 eyes, Imba 2 eyes - which you can see in action here.

By the way Imba 2 forced tabs make Imba 2 code look like total ass on github, as it uses 8 spaces for tab indentation, and OMG, it is ugly. By comparison 2-spaced Imba 1 code looks neat.

I know you can set your editor to display tabs as 2 spaces, but this setting won't apply everywhere (like GitHub for example, or blog posts). I think it's an absolutely terrible choice, and Imba should just switch to standard 2 spaces every other frontend tech uses, instead of trying to be different.

Imba 1 eyes code

tag Eye < svg:g
  prop mx
  prop my

  def render
    let max_eye_movement = 0.3 * data:sz
    let rx = data:x
    let ry = data:y
    if mx != null && my != null
      let dx = mx - data:x
      let dy = my - data:y
      let dl = Math.sqrt(dx*dx + dy*dy)
      if dl > max_eye_movement
        dx = max_eye_movement * dx/dl
        dy = max_eye_movement * dy/dl
      rx += dx
      ry += dy
    <self>
      <svg:circle.eye1 cx=(data:x) cy=(data:y) r=(data:sz)>
      <svg:circle.eye2 cx=(rx) cy=(ry) r=(data:sz * 0.5) css:fill=(data:color)>
      <svg:circle.eye3 cx=(rx) cy=(ry) r=(data:sz * 0.2)>

tag App
  def mount
    schedule(raf: true)

  def onmousemove(event)
    let native_event = event:_event
    let svg = document.get-element-by-id("eyes")
    let rect = svg.get-bounding-client-rect()
    @mx = native_event:pageX - rect:x
    @my = native_event:pageY - rect:y

  def eye_distance(eye1, eye2)
    let dx = eye1:x - eye2:x
    let dy = eye1:y - eye2:y
    Math.sqrt((dx * dx) + (dy * dy))

  def can_place_eye(new_eye)
    @eyes.every do |eye|
      eye_distance(eye, new_eye) >= eye:sz + new_eye:sz + 5

  def random_color
    let h = Math.random() * 360
    let s = Math.round(50 + Math.random() * 50)
    let l = Math.round(30 + Math.random() * 40)
    "hsl({h}, {s}%, {l}%)"

  def setup
    let wh = window:inner-height
    let ww = window:inner-width
    @mx = Math.random() * ww
    @my = Math.random() * wh
    @eyes = []
    for i in [1..1000]
      let sz = 20 + Math.random() * 60
      let x = sz + Math.random() * (ww - 2 * sz)
      let y = sz + Math.random() * (wh - 2 * sz)
      let new_eye = {x: x, y: y, sz: sz, color: random_color}
      if can_place_eye(new_eye)
        @eyes.push(new_eye)

  def render
    <self>
      <svg:svg#eyes>
        for eye in @eyes
          <Eye[eye] mx=@mx my=@my>

Imba.mount <App>
Enter fullscreen mode Exit fullscreen mode

Notable design here is that Eye component inherits from svg:g.

Imba 1 eyes scss

@import 'normalize-scss';
@include normalize();

body {
  overflow: hidden;
}

.App {
  width: 100vw;
  height: 100vh;
  overflow: hidden;

  svg {
    width: 100vw;
    height: 100vh;

    display: block;
    background-color: #aaa;

    .eye1 {
      fill: white;
      stroke: black;
      stroke-width: 3px;
    }
    .eye2 {
      stroke: black;
      stroke-width: 1px;
    }
    .eye3 {
      fill: black;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

It could have easily be plain CSS, but I just don't like plain CSS. Also using normalize from a package, the relevant parts would be just a few lines.

Imba 2 eyes code

I had to make every eye its own <svg> instead of just being a <g>. For this toy app it's fine, but there's a lot of cases where Imba 2's approach just won't do.

# NOTE:
# Can't inherit from svg:g yet in imba2
# so this is a bit awkward

tag spooky-eye
  def render
    let max_eye_movement = 0.3 * data.sz
    let rx = data.x
    let ry = data.y

    if mx != null && my != null
      let dx = mx - data.x
      let dy = my - data.y
      let dl = Math.sqrt(dx*dx + dy*dy)
      if dl > max_eye_movement
        dx = max_eye_movement * dx/dl
        dy = max_eye_movement * dy/dl
      rx += dx
      ry += dy

    <self>
      <svg>
        <svg:circle.eye1 cx=(data.x) cy=(data.y) r=(data.sz)>
        <svg:circle.eye2 cx=(rx) cy=(ry) r=(data.sz * 0.5) css:fill=(data.color)>
        <svg:circle.eye3 cx=(rx) cy=(ry) r=(data.sz * 0.2)>

tag app-root
  def eye_distance(eye1, eye2)
    let dx = eye1.x - eye2.x
    let dy = eye1.y - eye2.y
    Math.sqrt((dx * dx) + (dy * dy))

  def can_place_eye(new_eye)
    eyes.every do |eye|
      eye_distance(eye, new_eye) >= eye.sz + new_eye.sz + 5

  def random_color()
    let h = Math.random() * 360
    let s = Math.round(50 + Math.random() * 50)
    let l = Math.round(30 + Math.random() * 40)
    "hsl({h}, {s}%, {l}%)"

  def onmousemove(event)
    let element = document.get-element-by-id("eyes")
    let rect = element.get-bounding-client-rect()
    mx = event.page-x - rect.x
    my = event.page-y - rect.y

  def constructor
    super
    let wh = window.inner-height
    let ww = window.inner-width
    mx = Math.random() * ww
    my = Math.random() * wh
    eyes = []
    for i in [1 .. 1000]
      let sz = 20 + Math.random() * 60
      let x = sz + Math.random() * (ww - 2 * sz)
      let y = sz + Math.random() * (wh - 2 * sz)
      let new_eye = {x: x, y: y, sz: sz, color: random_color()}
      if can_place_eye(new_eye)
        eyes.push(new_eye)

  def render
    <self#eyes :mousemove.onmousemove>
      for eye in eyes
        <spooky-eye data=eye mx=mx my=my>
Enter fullscreen mode Exit fullscreen mode

Imba 2 eyes scss

I did not port that to Imba 2's new css system. I believe at the time I was doing the porting it wasn't there yet, so it just reuses the SCSS I had.

@import 'normalize-scss';
@include normalize();

app-root {
  display: block;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background-color: #aaa;

  svg {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    pointer-events: none;

    .eye1 {
      fill: white;
      stroke: black;
      stroke-width: 3px;
    }
    .eye2 {
      stroke: black;
      stroke-width: 1px;
    }
    .eye3 {
      fill: black;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Coming next

In the next few episodes I'll try to port a few more Imba 1 apps to Imba 2, and maybe try some of the new Imba 2's features like its new CSS system.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (0)

Retry later
Retry later