zorldo

Goofing around with Ebiten
git clone git://bsandro.tech/zorldo
Log | Files | Refs | README

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 }