ui_js.go (17521B)
1 // Copyright 2015 Hajime Hoshi 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package js 16 17 import ( 18 "syscall/js" 19 "time" 20 21 "github.com/hajimehoshi/ebiten/v2/internal/devicescale" 22 "github.com/hajimehoshi/ebiten/v2/internal/driver" 23 "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl" 24 "github.com/hajimehoshi/ebiten/v2/internal/hooks" 25 ) 26 27 var ( 28 stringNone = js.ValueOf("none") 29 stringTransparent = js.ValueOf("transparent") 30 ) 31 32 func driverCursorShapeToCSSCursor(cursor driver.CursorShape) string { 33 switch cursor { 34 case driver.CursorShapeDefault: 35 return "default" 36 case driver.CursorShapeText: 37 return "text" 38 case driver.CursorShapeCrosshair: 39 return "crosshair" 40 case driver.CursorShapePointer: 41 return "pointer" 42 case driver.CursorShapeEWResize: 43 return "ew-resize" 44 case driver.CursorShapeNSResize: 45 return "ns-resize" 46 } 47 return "auto" 48 } 49 50 type UserInterface struct { 51 runnableOnUnfocused bool 52 fpsMode driver.FPSMode 53 renderingScheduled bool 54 running bool 55 initFocused bool 56 cursorMode driver.CursorMode 57 cursorPrevMode driver.CursorMode 58 cursorShape driver.CursorShape 59 onceUpdateCalled bool 60 61 sizeChanged bool 62 63 lastDeviceScaleFactor float64 64 65 context driver.UIContext 66 input Input 67 } 68 69 var theUI = &UserInterface{ 70 runnableOnUnfocused: true, 71 sizeChanged: true, 72 initFocused: true, 73 } 74 75 func init() { 76 theUI.input.ui = theUI 77 } 78 79 func Get() *UserInterface { 80 return theUI 81 } 82 83 var ( 84 window = js.Global().Get("window") 85 document = js.Global().Get("document") 86 canvas js.Value 87 requestAnimationFrame = js.Global().Get("requestAnimationFrame") 88 setTimeout = js.Global().Get("setTimeout") 89 go2cpp = js.Global().Get("go2cpp") 90 ) 91 92 var ( 93 documentHasFocus js.Value 94 documentHidden js.Value 95 ) 96 97 func init() { 98 if go2cpp.Truthy() { 99 return 100 } 101 documentHasFocus = document.Get("hasFocus").Call("bind", document) 102 documentHidden = js.Global().Get("Object").Call("getOwnPropertyDescriptor", js.Global().Get("Document").Get("prototype"), "hidden").Get("get").Call("bind", document) 103 } 104 105 func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { 106 return window.Get("innerWidth").Int(), window.Get("innerHeight").Int() 107 } 108 109 func (u *UserInterface) SetFullscreen(fullscreen bool) { 110 if !canvas.Truthy() { 111 return 112 } 113 if !document.Truthy() { 114 return 115 } 116 if fullscreen == document.Get("fullscreenElement").Truthy() { 117 return 118 } 119 if fullscreen { 120 f := canvas.Get("requestFullscreen") 121 if !f.Truthy() { 122 f = canvas.Get("webkitRequestFullscreen") 123 } 124 f.Call("bind", canvas).Invoke() 125 return 126 } 127 f := document.Get("exitFullscreen") 128 if !f.Truthy() { 129 f = document.Get("webkitExitFullscreen") 130 } 131 f.Call("bind", document).Invoke() 132 } 133 134 func (u *UserInterface) IsFullscreen() bool { 135 if !document.Truthy() { 136 return false 137 } 138 if !document.Get("fullscreenElement").Truthy() && !document.Get("webkitFullscreenElement").Truthy() { 139 return false 140 } 141 return true 142 } 143 144 func (u *UserInterface) IsFocused() bool { 145 return u.isFocused() 146 } 147 148 func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) { 149 u.runnableOnUnfocused = runnableOnUnfocused 150 } 151 152 func (u *UserInterface) IsRunnableOnUnfocused() bool { 153 return u.runnableOnUnfocused 154 } 155 156 func (u *UserInterface) SetFPSMode(mode driver.FPSMode) { 157 u.fpsMode = mode 158 } 159 160 func (u *UserInterface) FPSMode() driver.FPSMode { 161 return u.fpsMode 162 } 163 164 func (u *UserInterface) ScheduleFrame() { 165 u.renderingScheduled = true 166 } 167 168 func (u *UserInterface) CursorMode() driver.CursorMode { 169 if !canvas.Truthy() { 170 return driver.CursorModeHidden 171 } 172 return u.cursorMode 173 } 174 175 func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { 176 if !canvas.Truthy() { 177 return 178 } 179 if u.cursorMode == mode { 180 return 181 } 182 // Remember the previous cursor mode in the case when the pointer lock exits by pressing ESC. 183 u.cursorPrevMode = u.cursorMode 184 if u.cursorMode == driver.CursorModeCaptured { 185 document.Call("exitPointerLock") 186 } 187 u.cursorMode = mode 188 switch mode { 189 case driver.CursorModeVisible: 190 canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape)) 191 case driver.CursorModeHidden: 192 canvas.Get("style").Set("cursor", stringNone) 193 case driver.CursorModeCaptured: 194 canvas.Call("requestPointerLock") 195 } 196 } 197 198 func (u *UserInterface) recoverCursorMode() { 199 if theUI.cursorPrevMode == driver.CursorModeCaptured { 200 panic("js: cursorPrevMode must not be driver.CursorModeCaptured at recoverCursorMode") 201 } 202 u.SetCursorMode(u.cursorPrevMode) 203 } 204 205 func (u *UserInterface) CursorShape() driver.CursorShape { 206 if !canvas.Truthy() { 207 return driver.CursorShapeDefault 208 } 209 return u.cursorShape 210 } 211 212 func (u *UserInterface) SetCursorShape(shape driver.CursorShape) { 213 if !canvas.Truthy() { 214 return 215 } 216 if u.cursorShape == shape { 217 return 218 } 219 220 u.cursorShape = shape 221 if u.cursorMode == driver.CursorModeVisible { 222 canvas.Get("style").Set("cursor", driverCursorShapeToCSSCursor(u.cursorShape)) 223 } 224 } 225 226 func (u *UserInterface) DeviceScaleFactor() float64 { 227 return devicescale.GetAt(0, 0) 228 } 229 230 func (u *UserInterface) updateSize() { 231 a := u.DeviceScaleFactor() 232 if u.lastDeviceScaleFactor != a { 233 u.updateScreenSize() 234 } 235 u.lastDeviceScaleFactor = a 236 237 if u.sizeChanged { 238 u.sizeChanged = false 239 switch { 240 case document.Truthy(): 241 body := document.Get("body") 242 bw := body.Get("clientWidth").Float() 243 bh := body.Get("clientHeight").Float() 244 u.context.Layout(bw, bh) 245 case go2cpp.Truthy(): 246 w := go2cpp.Get("screenWidth").Float() 247 h := go2cpp.Get("screenHeight").Float() 248 u.context.Layout(w, h) 249 default: 250 // Node.js 251 u.context.Layout(640, 480) 252 } 253 } 254 } 255 256 func (u *UserInterface) suspended() bool { 257 if u.runnableOnUnfocused { 258 return false 259 } 260 return !u.isFocused() 261 } 262 263 func (u *UserInterface) isFocused() bool { 264 if go2cpp.Truthy() { 265 return true 266 } 267 268 if !documentHasFocus.Invoke().Bool() { 269 return false 270 } 271 if documentHidden.Invoke().Bool() { 272 return false 273 } 274 return true 275 } 276 277 func (u *UserInterface) update() error { 278 if u.suspended() { 279 return hooks.SuspendAudio() 280 } 281 if err := hooks.ResumeAudio(); err != nil { 282 return err 283 } 284 return u.updateImpl(false) 285 } 286 287 func (u *UserInterface) updateImpl(force bool) error { 288 u.input.updateGamepads() 289 u.input.updateForGo2Cpp() 290 u.updateSize() 291 if force { 292 if err := u.context.ForceUpdateFrame(); err != nil { 293 return err 294 } 295 } else { 296 if err := u.context.UpdateFrame(); err != nil { 297 return err 298 } 299 } 300 return nil 301 } 302 303 func (u *UserInterface) needsUpdate() bool { 304 if u.fpsMode != driver.FPSModeVsyncOffMinimum { 305 return true 306 } 307 if !u.onceUpdateCalled { 308 return true 309 } 310 if u.renderingScheduled { 311 return true 312 } 313 // TODO: Watch the gamepad state? 314 return false 315 } 316 317 func (u *UserInterface) loop(context driver.UIContext) <-chan error { 318 u.context = context 319 320 errCh := make(chan error, 1) 321 reqStopAudioCh := make(chan struct{}) 322 resStopAudioCh := make(chan struct{}) 323 324 var cf js.Func 325 f := func() { 326 if u.needsUpdate() { 327 u.onceUpdateCalled = true 328 u.renderingScheduled = false 329 if err := u.update(); err != nil { 330 close(reqStopAudioCh) 331 <-resStopAudioCh 332 333 errCh <- err 334 return 335 } 336 } 337 switch u.fpsMode { 338 case driver.FPSModeVsyncOn: 339 requestAnimationFrame.Invoke(cf) 340 case driver.FPSModeVsyncOffMaximum: 341 setTimeout.Invoke(cf, 0) 342 case driver.FPSModeVsyncOffMinimum: 343 requestAnimationFrame.Invoke(cf) 344 } 345 } 346 347 // TODO: Should cf be released after the game ends? 348 cf = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 349 // f can be blocked but callbacks must not be blocked. Create a goroutine (#1161). 350 go f() 351 return nil 352 }) 353 354 // Call f asyncly since ch is used in f. 355 go f() 356 357 // Run another loop to watch suspended() as the above update function is never called when the tab is hidden. 358 // To check the document's visiblity, visibilitychange event should usually be used. However, this event is 359 // not reliable and sometimes it is not fired (#961). Then, watch the state regularly instead. 360 go func() { 361 defer close(resStopAudioCh) 362 363 const interval = 100 * time.Millisecond 364 t := time.NewTicker(interval) 365 defer func() { 366 t.Stop() 367 368 // This is a dirty hack. (*time.Ticker).Stop() just marks the timer 'deleted' [1] and 369 // something might run even after Stop. On Wasm, this causes an issue to execute Go program 370 // even after finishing (#1027). Sleep for the interval time duration to ensure that 371 // everything related to the timer is finished. 372 // 373 // [1] runtime.deltimer 374 time.Sleep(interval) 375 }() 376 377 for { 378 select { 379 case <-t.C: 380 if u.suspended() { 381 if err := hooks.SuspendAudio(); err != nil { 382 errCh <- err 383 return 384 } 385 } else { 386 if err := hooks.ResumeAudio(); err != nil { 387 errCh <- err 388 return 389 } 390 } 391 case <-reqStopAudioCh: 392 return 393 } 394 } 395 }() 396 397 return errCh 398 } 399 400 func init() { 401 // docuemnt is undefined on node.js 402 if !document.Truthy() { 403 return 404 } 405 406 if !document.Get("body").Truthy() { 407 ch := make(chan struct{}) 408 window.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 409 close(ch) 410 return nil 411 })) 412 <-ch 413 } 414 415 setWindowEventHandlers(window) 416 417 // Adjust the initial scale to 1. 418 // https://developer.mozilla.org/en/docs/Mozilla/Mobile/Viewport_meta_tag 419 meta := document.Call("createElement", "meta") 420 meta.Set("name", "viewport") 421 meta.Set("content", "width=device-width, initial-scale=1") 422 document.Get("head").Call("appendChild", meta) 423 424 canvas = document.Call("createElement", "canvas") 425 canvas.Set("width", 16) 426 canvas.Set("height", 16) 427 428 document.Get("body").Call("appendChild", canvas) 429 430 htmlStyle := document.Get("documentElement").Get("style") 431 htmlStyle.Set("height", "100%") 432 htmlStyle.Set("margin", "0") 433 htmlStyle.Set("padding", "0") 434 435 bodyStyle := document.Get("body").Get("style") 436 bodyStyle.Set("backgroundColor", "#000") 437 bodyStyle.Set("height", "100%") 438 bodyStyle.Set("margin", "0") 439 bodyStyle.Set("padding", "0") 440 441 canvasStyle := canvas.Get("style") 442 canvasStyle.Set("width", "100%") 443 canvasStyle.Set("height", "100%") 444 canvasStyle.Set("margin", "0") 445 canvasStyle.Set("padding", "0") 446 447 // Make the canvas focusable. 448 canvas.Call("setAttribute", "tabindex", 1) 449 canvas.Get("style").Set("outline", "none") 450 451 setCanvasEventHandlers(canvas) 452 453 // Pointer Lock 454 document.Call("addEventListener", "pointerlockchange", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 455 if document.Get("pointerLockElement").Truthy() { 456 return nil 457 } 458 // Recover the state correctly when the pointer lock exits. 459 460 // A user can exit the pointer lock by pressing ESC. In this case, sync the cursor mode state. 461 if theUI.cursorMode == driver.CursorModeCaptured { 462 theUI.recoverCursorMode() 463 } 464 theUI.input.recoverCursorPosition() 465 return nil 466 })) 467 document.Call("addEventListener", "pointerlockerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 468 js.Global().Get("console").Call("error", "pointerlockerror event is fired. 'sandbox=\"allow-pointer-lock\"' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change.") 469 return nil 470 })) 471 document.Call("addEventListener", "fullscreenerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 472 js.Global().Get("console").Call("error", "fullscreenerror event is fired. 'sandbox=\"fullscreen\"' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change.") 473 return nil 474 })) 475 document.Call("addEventListener", "webkitfullscreenerror", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 476 js.Global().Get("console").Call("error", "webkitfullscreenerror event is fired. 'sandbox=\"fullscreen\"' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change.") 477 return nil 478 })) 479 } 480 481 func setWindowEventHandlers(v js.Value) { 482 v.Call("addEventListener", "resize", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 483 theUI.updateScreenSize() 484 if err := theUI.updateImpl(true); err != nil { 485 panic(err) 486 } 487 return nil 488 })) 489 } 490 491 func setCanvasEventHandlers(v js.Value) { 492 // Keyboard 493 v.Call("addEventListener", "keydown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 494 // Focus the canvas explicitly to activate tha game (#961). 495 v.Call("focus") 496 497 e := args[0] 498 // Don't 'preventDefault' on keydown events or keypress events wouldn't work (#715). 499 theUI.input.updateFromEvent(e) 500 return nil 501 })) 502 v.Call("addEventListener", "keypress", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 503 e := args[0] 504 e.Call("preventDefault") 505 theUI.input.updateFromEvent(e) 506 return nil 507 })) 508 v.Call("addEventListener", "keyup", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 509 e := args[0] 510 e.Call("preventDefault") 511 theUI.input.updateFromEvent(e) 512 return nil 513 })) 514 515 // Mouse 516 v.Call("addEventListener", "mousedown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 517 // Focus the canvas explicitly to activate tha game (#961). 518 v.Call("focus") 519 520 e := args[0] 521 e.Call("preventDefault") 522 theUI.input.updateFromEvent(e) 523 return nil 524 })) 525 v.Call("addEventListener", "mouseup", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 526 e := args[0] 527 e.Call("preventDefault") 528 theUI.input.updateFromEvent(e) 529 return nil 530 })) 531 v.Call("addEventListener", "mousemove", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 532 e := args[0] 533 e.Call("preventDefault") 534 theUI.input.updateFromEvent(e) 535 return nil 536 })) 537 v.Call("addEventListener", "wheel", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 538 e := args[0] 539 e.Call("preventDefault") 540 theUI.input.updateFromEvent(e) 541 return nil 542 })) 543 544 // Touch 545 v.Call("addEventListener", "touchstart", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 546 // Focus the canvas explicitly to activate tha game (#961). 547 v.Call("focus") 548 549 e := args[0] 550 e.Call("preventDefault") 551 theUI.input.updateFromEvent(e) 552 return nil 553 })) 554 v.Call("addEventListener", "touchend", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 555 e := args[0] 556 e.Call("preventDefault") 557 theUI.input.updateFromEvent(e) 558 return nil 559 })) 560 v.Call("addEventListener", "touchmove", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 561 e := args[0] 562 e.Call("preventDefault") 563 theUI.input.updateFromEvent(e) 564 return nil 565 })) 566 567 // Context menu 568 v.Call("addEventListener", "contextmenu", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 569 e := args[0] 570 e.Call("preventDefault") 571 return nil 572 })) 573 574 // Context 575 v.Call("addEventListener", "webglcontextlost", js.FuncOf(func(this js.Value, args []js.Value) interface{} { 576 e := args[0] 577 e.Call("preventDefault") 578 window.Get("location").Call("reload") 579 return nil 580 })) 581 } 582 583 func (u *UserInterface) forceUpdateOnMinimumFPSMode() { 584 if u.fpsMode != driver.FPSModeVsyncOffMinimum { 585 return 586 } 587 u.updateImpl(true) 588 } 589 590 func (u *UserInterface) Run(context driver.UIContext) error { 591 if u.initFocused && window.Truthy() { 592 // Do not focus the canvas when the current document is in an iframe. 593 // Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373). 594 isInIframe := !window.Get("location").Equal(window.Get("parent").Get("location")) 595 if !isInIframe { 596 canvas.Call("focus") 597 } 598 } 599 u.running = true 600 return <-u.loop(context) 601 } 602 603 func (u *UserInterface) RunWithoutMainLoop(context driver.UIContext) { 604 panic("js: RunWithoutMainLoop is not implemented") 605 } 606 607 func (u *UserInterface) updateScreenSize() { 608 switch { 609 case document.Truthy(): 610 body := document.Get("body") 611 bw := int(body.Get("clientWidth").Float() * u.DeviceScaleFactor()) 612 bh := int(body.Get("clientHeight").Float() * u.DeviceScaleFactor()) 613 canvas.Set("width", bw) 614 canvas.Set("height", bh) 615 case go2cpp.Truthy(): 616 // TODO: Implement this 617 } 618 u.sizeChanged = true 619 } 620 621 func (u *UserInterface) SetScreenTransparent(transparent bool) { 622 if u.running { 623 panic("js: SetScreenTransparent can't be called after the main loop starts") 624 } 625 626 bodyStyle := document.Get("body").Get("style") 627 if transparent { 628 bodyStyle.Set("backgroundColor", "transparent") 629 } else { 630 bodyStyle.Set("backgroundColor", "#000") 631 } 632 } 633 634 func (u *UserInterface) IsScreenTransparent() bool { 635 bodyStyle := document.Get("body").Get("style") 636 return bodyStyle.Get("backgroundColor").Equal(stringTransparent) 637 } 638 639 func (u *UserInterface) ResetForFrame() { 640 u.updateSize() 641 u.input.resetForFrame() 642 } 643 644 func (u *UserInterface) SetInitFocused(focused bool) { 645 if u.running { 646 panic("ui: SetInitFocused must be called before the main loop") 647 } 648 u.initFocused = focused 649 } 650 651 func (u *UserInterface) Input() driver.Input { 652 return &u.input 653 } 654 655 func (u *UserInterface) Window() driver.Window { 656 return nil 657 } 658 659 func (*UserInterface) Graphics() driver.Graphics { 660 return opengl.Get() 661 }