Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Canvas and TimelineView to DOM renderer #449

Merged
merged 11 commits into from Sep 28, 2021
Merged

Add Canvas and TimelineView to DOM renderer #449

merged 11 commits into from Sep 28, 2021

Conversation

carson-katri
Copy link
Member

@carson-katri carson-katri commented Sep 19, 2021

This is built on the HTML canvas, and is compatible with the iOS 15 Canvas view. TimelineView with an .animation schedule only runs with Canvas at the moment with a special implementation using requestAnimationFrame, but could be adjusted to work with other views as well.

Here's a demo of confetti using Canvas:

CanvasDemo.mov

@carson-katri carson-katri added SwiftUI compatibility Tokamak API differences with SwiftUI DOM/HTML renderer Tokamak in the browser labels Sep 19, 2021
storage: .ellipse(
.init(
x: rect.origin
.x + (rect.width > rect.height ? (rect.width / 2) - (rect.width - rect.height) : 0),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments in code explaining why this change is needed?

}
}

private func draw() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be an easy way to split this into multiple functions to make it more readable?

}
}

private func pushPath(_ path: Path, in canvasContext: JSObject) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

canvas
.font =
.string(
"\(style) \(variant) \(weight) \(size ?? "17")pt \(lineHeight) \(family ?? Font.Design.default.families.joined(separator: " "))"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"\(style) \(variant) \(weight) \(size ?? "17")pt \(lineHeight) \(family ?? Font.Design.default.families.joined(separator: " "))"
"""
\(style) \(variant) \(weight) \(size ?? "17")pt \(lineHeight) \(
family ?? Font.Design.default.families.joined(separator: " ")
)
"""

Comment on lines 420 to 426
return .string(AnyColorBox.ResolvedValue(
red: color.red,
green: color.green,
blue: color.blue,
opacity: color.opacity * Double(opacity),
space: color.space
).cssValue)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could moved into some extension on AnyColorBox.ResolvedValue that has an initializer taking a color value and supplied opacity. That would reduce these 6 lines to 1 with just that helper initializer call, also potentially resolving the linter warning.

}
}

private extension GraphicsContext.Shading {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extensions below this line could into a separate file? Yes, they would no longer stay private, but it would potentially improve readability and overall navigation through the project, preventing files from becoming larger.

Copy link
Collaborator

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marvellous! 🎊

Other than a few linter nits, maybe there's a way to add an HTML snapshot test? Albeit primitive it could ensure that at least the <canvas> tag is rendered.

@MaxDesiatov
Copy link
Collaborator

Disregard the snapshot test comment, I realize this won't be possible right now because we can't test TokamakDOM directly, and this is implemented in TokamakDOM.

@carson-katri
Copy link
Member Author

carson-katri commented Sep 23, 2021

Haven't done much cleanup yet, but did add support for symbols:

Canvas { context, size in
  // Draw axes on the canvas.
  context.stroke(Rectangle().path(in: .init(x: size.width / 2, y: 0, width: 1, height: size.height)), with: .color(.green))
  context.stroke(Rectangle().path(in: .init(x: 0, y: size.height / 2, width: size.width, height: 1)), with: .color(.green))
  
  // Resolve and draw the symbols, looking them up by tag.
  guard let aSymbol = context.resolveSymbol(id: "a"),
        let bSymbol = context.resolveSymbol(id: "b") else { return }
  context.draw(aSymbol, at: .init(x: size.width / 2, y: size.height / 2), anchor: .topLeading)
  context.draw(bSymbol, at: .init(x: size.width / 2, y: size.height / 2), anchor: .bottomTrailing)
} symbols: {
  Text("A")
    .frame(width: 100, height: 100)
    .background(Color.red)
    .cornerRadius(10)
    .tag("a") // Tagged with "a"
  Text("B")
    .frame(width: 100, height: 100)
    .background(Color.blue)
    .tag("b") // Tagged with "b"
}

Screen Shot 2021-09-23 at 2 59 15 PM

To look up Views by their tag, I added support for _VariadicView (matching SwiftUI's) which provides access to child views as a RandomAccessCollection, as well as access to _ViewTraitKeys. The basic usage in Canvas is like so:

private struct SymbolResolverLayout<ID: Hashable>: _VariadicView.ViewRoot {
  let id: ID

  func body(children: _VariadicView.Children) -> some View {
    ForEach(children) {
      if case let .tagged(tag) = $0[TagValueTraitKey<ID>.self],
         tag == id
      {
        $0
      }
    }
  }
}

// Then used like so:
_VariadicView.Tree(SymbolResolverLayout(id: id)) {
  _storage.symbols
}

The views are rendered in the canvas using SVG+foreignObject.

@carson-katri
Copy link
Member Author

Off-topic, but _VariadicView could be used to solve some issues with Picker. It can allow for the use of tag(_:) to specify items alongside ForEach:

Picker("Choose...", selection: $selection) {
  ForEach(items) { Text($0.title) }
  Text("Clear").tag(Item.clear)
}

or for re-tagging:

Picker("Choose...", selection: $value) {
  ForEach(items) {
    Text($0.title)
      .tag($0.value)
  }
}

@MaxDesiatov
Copy link
Collaborator

Would this be ready for review soon? It would be great to keep the PR relatively small, and then additional features could be implemented in a separate PR, for easier review, and to have separate commits for such features in the main branch history.

@carson-katri
Copy link
Member Author

carson-katri commented Sep 25, 2021

Yes, it can be reviewed now.

@carson-katri carson-katri requested a review from a team September 25, 2021 19:54
@@ -0,0 +1,96 @@
// Copyright 2019-2020 Tokamak contributors
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2019-2020 Tokamak contributors
// Copyright 2021 Tokamak contributors

Copy link
Collaborator

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just a few nits

.github/workflows/ci.yml Show resolved Hide resolved
Sources/TokamakCore/Shapes/Rectangle.swift Outdated Show resolved Hide resolved
Sources/TokamakCore/Views/Canvas/TimelineView.swift Outdated Show resolved Hide resolved
Sources/TokamakDemo/CanvasDemo.swift Outdated Show resolved Hide resolved
Copy link
Member

@ezraberch ezraberch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. 👍

@carson-katri carson-katri merged commit 0059962 into main Sep 28, 2021
@carson-katri carson-katri deleted the canvas branch September 28, 2021 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DOM/HTML renderer Tokamak in the browser SwiftUI compatibility Tokamak API differences with SwiftUI
Development

Successfully merging this pull request may close these issues.

None yet

3 participants