Closed gepengscu closed 2 years ago
I didn't check your code but I use a loop with app.Window().Call("requestAnimationFrame", renderFrame)
in an async function to render my canvas example (a simple pong game) and stop it using a channel on dismount.
You could also try my PR #711 and use "ctx.SkipAllUpdates()" in your "OnMouseMove()" handler. That is what I would try first.
You can do this by adding:
replace github.com/maxence-charriere/go-app/v9 => github.com/metatexx/go-app/v9 v9.4.2-0.20220405155713-0d5b7519f640
at the end of your go mod file (which also contains all my other PRs like loading progress indicator and custom tags).
I still had some time before I have to leave the office so I quickly tried your canvas example and even with just your original code, I don't see the performance problem. What am I supposed to find a problem with it?
This is my canvas example:
(both of them are only at about 30 fps because of the recorder)
Thank you very much for your quick reply!Did you compare it with the HTML+js example?
When the mouse moves, the flicker is almost invisible in the HTML example, but it is more obvious in the app.Canvas example.
What flicker? I recreated your whole javascript version:
package canvas
import (
"github.com/maxence-charriere/go-app/v9/pkg/app"
"math"
)
type Ctrl struct {
app.Compo
width int
height int
fCanvas app.Value
bgCanvas app.Value
}
func (c *Ctrl) OnInit() {
c.width = 400
c.height = 400
}
func (c *Ctrl) OnMount(ctx app.Context) {
c.fCanvas = app.Window().GetElementByID("canvas").Call("getContext", "2d")
bkg := app.Window().Get("document").Call("createElement", "canvas")
bkg.Set("width", c.width)
bkg.Set("height", c.height)
bgc := bkg.Call("getContext", "2d")
bgc.Call("beginPath")
bgc.Call("arc", c.width/2, c.height/2, 100, 0, math.Pi*2)
bgc.Set("fillStyle", "yellow")
bgc.Call("fillRect", 100, 10, 40, 30)
bgc.Call("stroke")
c.bgCanvas = bkg
}
func (c *Ctrl) Render() app.UI {
return app.Div().Body(app.Canvas().
ID("canvas").
Width(c.width).
Height(c.height).
OnMouseMove(
func(ctx app.Context, e app.Event) {
x := e.Get("offsetX").Int()
y := e.Get("offsetY").Int()
c.fCanvas.Call("clearRect", 0, 0, c.width, c.height)
if c.bgCanvas.Truthy() {
c.fCanvas.Call("drawImage", c.bgCanvas, x-c.width/2, y-c.height/2, c.width, c.height)
}
c.fCanvas.Call("beginPath")
c.fCanvas.Call("moveTo", 0, y)
c.fCanvas.Call("lineTo", c.width, y)
c.fCanvas.Call("moveTo", x, 0)
c.fCanvas.Call("lineTo", x, c.height)
c.fCanvas.Call("stroke")
c.fCanvas.Call("beginPath")
c.fCanvas.Call("ellipse", x, y, 100, 100, math.Pi/4, 0, 2*math.Pi)
c.fCanvas.Call("stroke")
//ctx.SkipAllUpdates()
},
))
}
This is a screen recording of the HTML and the Go-App variant:
https://user-images.githubusercontent.com/719156/162220832-bcc22645-3634-4031-814c-9c9c9d66d828.mp4
But if you really want "no flicker" I still think you should use requestAnimationFrame
and render each (possible) frame in there.
Here is my little pong example (it scales and keeps timing but this is not perfect):
package canvas
import (
"math"
"math/rand"
"github.com/google/uuid"
"github.com/metatexx/go-app-pkgs/dbg"
"github.com/maxence-charriere/go-app/v9/pkg/app"
)
type renderChan chan struct{}
type Canvas struct {
app.Compo
Width float64
Height float64
Id string
done renderChan
running bool
}
var _ app.Composer = (*Canvas)(nil)
var _ app.Mounter = (*Canvas)(nil)
var _ app.Dismounter = (*Canvas)(nil)
func (c *Canvas) OnInit() {
dbg.Log()
c.Id = "cvs:" + uuid.NewString()
}
func (c *Canvas) OnDismount() {
dbg.Log()
// tell our go routine that we don't need it anymore
c.running = false // comment this out to see how it gets ugly :)
}
func (c *Canvas) OnMount(ctx app.Context) {
dbg.Log()
cvEl := app.Window().GetElementByID(c.Id)
cvCtx := cvEl.Call("getContext", "2d")
// run "endless"
c.done = make(chan struct{})
ctx.Async(func() {
scale := c.Width / 640.0
s := 10.0 * scale
x := float64(rand.Intn(int(640-50))+25) * scale
y := float64(rand.Intn(int(480-10))+5) * scale
ph := 80.0 * c.Width / 640
speed := 200.0 * scale
dx, dy := 0.21, 0.42 // speed
dx += float64(rand.Intn(100)/100) - 0.5
w := cvEl.Get("width").Float()
h := cvEl.Get("height").Float()
var renderFrame app.Func
var tlast float64
c.running = true
var tcount = 0
renderFrame = app.FuncOf(func(this app.Value, args []app.Value) interface{} {
now := args[0].Float()
tdiff := now - tlast
tlast = now
tcount++
if tcount%100 == 0 {
dbg.Logf("count %d", tcount)
}
cvCtx.Call("clearRect", 0, 0, w, h)
cvCtx.Call("beginPath")
cvCtx.Set("globalAlpha", 1)
cvCtx.Set("lineWidth", 5*scale)
cvCtx.Set("strokeStyle", "#202020")
cvCtx.Set("fillStyle", "#e0e0e0")
cvCtx.Call("rect", 0, 0, w, h)
cvCtx.Call("fill")
cvCtx.Call("stroke")
cvCtx.Call("beginPath")
cvCtx.Call("setLineDash", []interface{}{5})
cvCtx.Set("globalAlpha", 0.5)
cvCtx.Set("lineWidth", 3*scale)
cvCtx.Call("moveTo", w/2+2.5, 0)
cvCtx.Call("lineTo", w/2+2.5, h)
cvCtx.Call("stroke")
ppos := y - ph/2
if ppos < 5 {
ppos = 5
} else if (ppos + ph) > h-5 {
ppos = h - ph - 5
}
cvCtx.Call("beginPath")
cvCtx.Call("setLineDash", []interface{}{})
cvCtx.Set("fillStyle", "#30A030")
cvCtx.Set("lineWidth", 2)
cvCtx.Set("globalAlpha", 1.0)
cvCtx.Call("rect", 15*scale, ppos, 10*scale, ph)
cvCtx.Call("fill")
cvCtx.Call("beginPath")
cvCtx.Set("lineWidth", 2)
cvCtx.Set("fillStyle", "#3030A0")
cvCtx.Call("rect", w-25*scale, ppos, 10*scale, ph)
cvCtx.Call("fill")
cvCtx.Set("globalAlpha", 1.0)
cvCtx.Call("beginPath")
cvCtx.Set("fillStyle", "#A00000")
cvCtx.Set("strokeStyle", "#000000")
cvCtx.Call("setLineDash", []interface{}{})
cvCtx.Call("arc", x, y, s, 0, 2*math.Pi)
cvCtx.Call("fill")
sdx := dx * speed / tdiff
sdy := dy * speed / tdiff
if (x+sdx) > w-25*scale-s || (x+sdx) < s+25*scale {
dx *= -1
sdx *= -1
}
if (y+sdy) > h-s || (y+sdy) < s {
dy *= -1
sdy *= -1
}
x += sdx
y += sdy
if c.running {
app.Window().Call("requestAnimationFrame", renderFrame)
} else {
close(c.done)
}
return nil
})
defer renderFrame.Release()
// start the rendering
app.Window().Call("requestAnimationFrame", renderFrame)
// wait till the channel gets closed
<-c.done
dbg.Log("closed")
})
}
func (c *Canvas) Render() app.UI {
dbg.Log()
if c.Width == 0 {
if c.Height == 0 {
c.Width = 640
} else {
c.Width = math.Floor(c.Height / 3 * 4)
}
}
if c.Height == 0 {
if c.Width == 0 {
c.Height = 480
} else {
c.Height = math.Floor(c.Width / 4 * 3)
}
}
return app.Canvas().ID(c.Id).Width(int(c.Width)).Height(int(c.Height))
}
And if you still have problems with the performance, just use the Javascript version in your app. There is no reason which keeps you back to using god old Javascript. This is one of the reasons, why we actually consider using go-app for clients (if the upfront load time is accepted).
I think I found the reason. The test component does not blink when running alone, but it appears when the component is embedded in a slightly more complex page. I'm trying to build a component library and migrate an old C / s system. This test component is embedded in a large test demo. When canvas is updated, all the parent components are updated. I'm looking forward to your Skipallupdates PR, which may solve this problem.
I'm looking forward to your Skipallupdates PR, which may solve this problem.
Just try it out. It's just adding the line in the mod file. I would love to find more people supporting this PR.
The problem is that the parent component has not been modified. If I only update the canvas component, other components should not be updated according to the "deep compare" logic of go app. The parent component seems to be updated only because the style has been changed from width = "100%" height = "100%" to height = "100%" width = "100%" Is it because the map is unordered? But "deep compare" traverses the keys for map object.
should not be updated
I am not sure what you mean by updated. Go-App as-is will call render on your component and any parent to create the "updated state" and then should only modify the dom when something is changed when compared to that new state.
Besides all of this, I still believe it is not the right thing to render on mousemoves instead on frame changes.
For some reason, onMouseMove triggered wasm exec. js:22. When the mouse moves, wasm exec. js:22 constantly prints empty information on the console, which consumes a lot of resources, resulting in the aforementioned refresh delay problem.
This wasm_exec seems to be from the filesystem layer "writeSync()" which writes to the console.
I believe that you actually have a bug in your code. I do not get any output from the wasm_exe.js besides my debugging and the service worker logging when running apps. This includes using OnMouseMove()
and running the component nested deep as a child of my application frameworks controller component.
Maybe you can add dbg.Log()
in your code to narrow down where the output is generated. I have dbg.Log()
in the head of about every single component method, so I can see exactly what is getting called and in which order. It loogs the calling function and file so it is easy to trace things with it.
It looks similar to this:
main.(*appControl).Render
main.(*TplSide).Render: form
github.com/metatexx/skelly/pkg/components/combobox.(*ComboBox).Render
github.com/metatexx/skelly/pkg/components/combobox.(*ComboBox).apiGetOptions:
github.com/metatexx/skelly/pkg/components/combobox.(*ComboBox).Render
github.com/metatexx/skelly/pkg/pages.(*Form).OnMount
github.com/metatexx/skelly/pkg/components/combobox.(*ComboBox).Render
I also have it in my canvas example earlier in the comments.
The problem is found. There is an fmt.Println hidden in the range. Anyway, thank you very much!
app.Range(this.FItems).Slice(
func(i int) app.UI {
ui := this.FItems[i].(IMenuNode)
fmt.Println(ui.Title) // bug here
ui.SetMenu(this)
ui.SetIdent(this.FIdent)
return this.FItems[i].(app.UI)
}),
This is a test example based on your suggestion. Very nice!
type Ctrl3 struct {
app.Compo
fCanvas app.Value
bgCanvas app.Value
width int
height int
x int
y int
running bool
}
func (this *Ctrl3) OnInit() {
this.width = 800
this.height = 800
}
func (c *Ctrl3) OnMount(ctx app.Context) {
c.fCanvas = app.Window().GetElementByID("canvas")
c.invalid(ctx)
}
func (c *Ctrl3) invalid(ctx app.Context) {
if c.running {
return
}
ctx.Async(
func() {
done := make(chan struct{})
var renderFrame app.Func
renderFrame = app.FuncOf(
func(v app.Value, args []app.Value) interface{} {
c.running = true
if c.fCanvas.Truthy() {
c.paint(graph.Canvas(c.fCanvas))
}
close(done)
return nil
})
defer renderFrame.Release()
// start the rendering
app.Window().Call("requestAnimationFrame", renderFrame)
<-done
c.running = false
})
}
func (c *Ctrl3) paint(canvas graph.ICanvas) {
canvas.ClearRect(0, 0, c.width, c.height).
BeginPath().
MoveTo(0, 0).
LineTo(0, c.height).
LineTo(c.width, c.height).
LineTo(c.width, 0).
LineTo(0, 0).
LineTo(c.x, c.y).
MoveTo(c.x, 0).
LineTo(c.x, c.height).
MoveTo(0, c.y).
LineTo(c.width, c.y).
Stroke()
canvas.BeginPath().MoveTo(c.x, c.y)
for i := 0; i < 10000; i++ {
x, y := rand.Intn(c.width), rand.Intn(c.width)
canvas.LineTo(x, y).Arc(x, y, rand.Intn(20), 0, math.Pi*2)
}
canvas.Stroke()
}
func (c *Ctrl3) Render() app.UI {
return app.Canvas().
ID("canvas").
Style("cursor", "none").
Width(c.width).
Height(c.height).
OnMouseMove(
func(ctx app.Context, e app.Event) {
c.x = e.Get("offsetX").Int()
c.y = e.Get("offsetY").Int()
c.invalid(ctx)
},
)
}
These are two examples of canvas. One is written in HTML+js and the other is written in go-app with app.Canvas. Is there any way to optimize the performance of app.Canvas?
This is html+js.
This is go-app.