@lightningjs/blits 2.14.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -58,8 +58,8 @@
58
58
  },
59
59
  "scripts": {
60
60
  "test": "c8 npm run test:run | tap-diff",
61
- "test:run": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\"",
62
- "test:run:failures": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" | tap-filter test-fail | tap-diff",
61
+ "test:run": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" \"vite/**/*.test.js\"",
62
+ "test:run:failures": "node -r global-jsdom/register node_modules/tape/bin/tape \"scripts/**/*.test.js\" \"src/**/*.test.js\" \"vite/**/*.test.js\" | tap-filter test-fail | tap-diff",
63
63
  "lint": "node scripts/lint.js",
64
64
  "lint:fix": "node scripts/lint.js fix",
65
65
  "prepublishOnly": "node scripts/prepublishOnly.js",
@@ -70,7 +70,7 @@
70
70
  "bench:compare": "npm --prefix benchmarks run bench:compare --",
71
71
  "bench:profile": "npm --prefix benchmarks run bench:profile --",
72
72
  "bench:serve": "npm --prefix benchmarks run serve",
73
- "release": "npm publish --access public"
73
+ "release": "node scripts/release.js"
74
74
  },
75
75
  "types": "./index.d.ts",
76
76
  "lint-staged": {
@@ -95,7 +95,7 @@
95
95
  "globals": "^17.12.0",
96
96
  "husky": "^9.1.7",
97
97
  "jsdom": "^30.1.0",
98
- "lint-staged": "^15.5.2",
98
+ "lint-staged": "^17.6.0",
99
99
  "prettier": "^3.6.2",
100
100
  "sinon": "^22.1.0",
101
101
  "tap-diff": "^0.1.1",
@@ -103,7 +103,7 @@
103
103
  "tape": "^5.9.0"
104
104
  },
105
105
  "dependencies": {
106
- "@lightningjs/renderer": "^3.7.0",
106
+ "@lightningjs/renderer": "3.11.0",
107
107
  "magic-string": "^1.4.1"
108
108
  },
109
109
  "repository": {
@@ -24,16 +24,79 @@ import symbols from './lib/symbols.js'
24
24
  import { DEFAULT_HOLD_TIMEOUT_MS, DEFAULT_KEYMAP } from './constants.js'
25
25
  import { renderer } from './launch.js'
26
26
  import { platform } from './platform.js'
27
+ import { keyMap, setKeyMap } from './keymap.js'
27
28
 
28
- /**
29
- * Merged keyMap (default + custom settings).
30
- * Initialized once during application init.
31
- * @type {Object<string, string>}
32
- */
33
- export let keyMap = {}
29
+ export { keyMap } from './keymap.js'
30
+
31
+ // Reacts to Blits-only settings changed at runtime via `this.$settings.set()`.
32
+ Settings.onRuntimeSettingChange = (key, value) => {
33
+ if (key === 'keymap') {
34
+ initKeyMap()
35
+ return
36
+ }
37
+
38
+ if (key === 'inputThrottle') {
39
+ inputThrottleMs = value
40
+ return
41
+ }
42
+
43
+ if (key === 'mouseMoveThrottle') {
44
+ mouseMoveThrottleMs = value
45
+ return
46
+ }
47
+
48
+ if (
49
+ key === 'announcerOptions' &&
50
+ liveAppInstance &&
51
+ liveAppInstance.$announcer &&
52
+ value &&
53
+ typeof value === 'object'
54
+ ) {
55
+ liveAppInstance.$announcer.configure(value)
56
+ return
57
+ }
58
+
59
+ if (key === 'enableMouse') {
60
+ enableMouseFlag = value
61
+ if (liveInput !== null && liveViewport !== null) {
62
+ if (value === true && mouseListenersAttached === false) {
63
+ liveInput.addEventListener('mousemove', mouseMoveHandlerRef)
64
+ liveInput.addEventListener('click', mouseClickHandlerRef)
65
+ liveViewport.addEventListener('resize', updateCanvasRectRef)
66
+ liveViewport.addEventListener('scroll', updateCanvasRectRef)
67
+ mouseListenersAttached = true
68
+ } else if (value === false && mouseListenersAttached === true) {
69
+ liveInput.removeEventListener('mousemove', mouseMoveHandlerRef)
70
+ liveInput.removeEventListener('click', mouseClickHandlerRef)
71
+ liveViewport.removeEventListener('resize', updateCanvasRectRef)
72
+ liveViewport.removeEventListener('scroll', updateCanvasRectRef)
73
+ mouseListenersAttached = false
74
+ Hover.clear()
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ /** @type {any} Active App component instance, set during App init */
81
+ let liveAppInstance = null
82
+ /** @type {any} Platform input target, set during App init */
83
+ let liveInput = null
84
+ /** @type {any} Platform viewport target, set during App init */
85
+ let liveViewport = null
86
+ let mouseListenersAttached = false
87
+ /** @type {((e: any) => void)|null} */
88
+ let mouseMoveHandlerRef = null
89
+ /** @type {(() => void)|null} */
90
+ let mouseClickHandlerRef = null
91
+ /** @type {(() => void)|null} */
92
+ let updateCanvasRectRef = null
93
+
94
+ let inputThrottleMs = 0
95
+ let mouseMoveThrottleMs = 100
96
+ let enableMouseFlag = false
34
97
 
35
98
  export const initKeyMap = () => {
36
- keyMap = { ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) }
99
+ setKeyMap({ ...DEFAULT_KEYMAP, ...Settings.get('keymap', {}) })
37
100
  }
38
101
 
39
102
  const Application = (config) => {
@@ -66,6 +129,7 @@ const Application = (config) => {
66
129
  }
67
130
 
68
131
  config.hooks[symbols.init] = function () {
132
+ liveAppInstance = this
69
133
  // set the initial activation state of the announcer based on launch setting
70
134
  this.$announcer.toggle(Settings.get('announcer', false))
71
135
  // configure global default utterance options if provided in settings
@@ -75,15 +139,15 @@ const Application = (config) => {
75
139
  }
76
140
  // Initialize merged keyMap once during application init
77
141
  initKeyMap()
78
-
79
- /** @type {number} Input throttle time in milliseconds (0 = disabled) */
80
- const throttleMs = Settings.get('inputThrottle', 0)
81
-
82
- const mouseEnabled = Settings.get('enableMouse', false)
83
- const mouseMoveThrottle = Settings.get('mouseMoveThrottle', 100)
142
+ // Initialize fast-path cached settings once during application init
143
+ inputThrottleMs = Settings.get('inputThrottle', 0)
144
+ mouseMoveThrottleMs = Settings.get('mouseMoveThrottle', 100)
145
+ enableMouseFlag = Settings.get('enableMouse', false)
84
146
  const { createKeyboardEvent, input, isKeyboardEvent, now = Date.now, viewport } = platform
147
+ liveInput = input
148
+ liveViewport = viewport
85
149
 
86
- keyDownHandler = async (e) => {
150
+ keyDownHandler = async (/** @type {any} */ e) => {
87
151
  const currentTime = now()
88
152
 
89
153
  const key = keyMap[e.keyCode] || e.keyCode
@@ -91,11 +155,11 @@ const Application = (config) => {
91
155
  lastInputKey = key
92
156
  // execute immediately when no throttle is specified or event is internal (bubbled up by focus manager)
93
157
  // or key is different from the last used key
94
- if (throttleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
158
+ if (inputThrottleMs === 0 || e[symbols.internalEvent] === true || sameKey === false) {
95
159
  return await processInput.call(this, e, key)
96
160
  }
97
161
 
98
- if (currentTime - lastInputTime < throttleMs) {
162
+ if (currentTime - lastInputTime < inputThrottleMs) {
99
163
  return
100
164
  }
101
165
 
@@ -115,7 +179,7 @@ const Application = (config) => {
115
179
  }
116
180
 
117
181
  Focus.input(key, e)
118
- if (mouseEnabled === true) {
182
+ if (enableMouseFlag === true) {
119
183
  Hover.clear()
120
184
  // forget the node under the pointer too, so moving on it hovers it again
121
185
  currentNode = undefined
@@ -130,6 +194,9 @@ const Application = (config) => {
130
194
  }
131
195
 
132
196
  keyUpHandler = (e) => {
197
+ clearTimeout(holdTimeout)
198
+ Focus.hold = false
199
+
133
200
  const entry = keyUpCallbacks.get(e.keyCode)
134
201
  if (entry !== undefined) {
135
202
  keyUpCallbacks.delete(e.keyCode)
@@ -137,8 +204,6 @@ const Application = (config) => {
137
204
  entry.callback()
138
205
  }
139
206
  }
140
- clearTimeout(holdTimeout)
141
- Focus.hold = false
142
207
  }
143
208
 
144
209
  let lastMoved = 0
@@ -154,7 +219,7 @@ const Application = (config) => {
154
219
 
155
220
  // limit the amount of move events per time frame
156
221
  mouseMoveHandler = (e) => {
157
- if (e.timeStamp - lastMoved < mouseMoveThrottle) return
222
+ if (e.timeStamp - lastMoved < mouseMoveThrottleMs) return
158
223
  lastMoved = e.timeStamp
159
224
 
160
225
  this.$emit('mouse::move', e)
@@ -166,7 +231,7 @@ const Application = (config) => {
166
231
  return
167
232
  }
168
233
 
169
- const stage = renderer.stage
234
+ const stage = /** @type {any} */ (renderer).stage
170
235
  if (stage == null) {
171
236
  return
172
237
  }
@@ -179,6 +244,7 @@ const Application = (config) => {
179
244
 
180
245
  if (node === null) {
181
246
  currentComponent = undefined
247
+ currentNode = null
182
248
  Hover.clear()
183
249
  return
184
250
  }
@@ -215,16 +281,21 @@ const Application = (config) => {
215
281
  input.addEventListener('keydown', keyDownHandler)
216
282
  input.addEventListener('keyup', keyUpHandler)
217
283
  inputCleanup = () => {
218
- input.removeEventListener('keydown', keyDownHandler)
219
- input.removeEventListener('keyup', keyUpHandler)
284
+ input.removeEventListener('keydown', /** @type {any} */ (keyDownHandler))
285
+ input.removeEventListener('keyup', /** @type {any} */ (keyUpHandler))
220
286
  }
221
287
  }
222
- if (mouseEnabled === true && input !== undefined && viewport !== undefined) {
288
+ mouseMoveHandlerRef = mouseMoveHandler
289
+ mouseClickHandlerRef = mouseClickHandler
290
+ updateCanvasRectRef = updateCanvasRect
291
+
292
+ if (enableMouseFlag === true && input !== undefined && viewport !== undefined) {
223
293
  updateCanvasRect()
224
294
  input.addEventListener('mousemove', mouseMoveHandler)
225
295
  input.addEventListener('click', mouseClickHandler)
226
296
  viewport.addEventListener('resize', updateCanvasRect)
227
297
  viewport.addEventListener('scroll', updateCanvasRect)
298
+ mouseListenersAttached = true
228
299
  mouseCleanup = () => {
229
300
  input.removeEventListener('mousemove', mouseMoveHandler)
230
301
  input.removeEventListener('click', mouseClickHandler)
@@ -233,8 +304,9 @@ const Application = (config) => {
233
304
  }
234
305
  }
235
306
 
236
- // next tick
237
- setTimeout(() => Focus.set(this))
307
+ // focus once the component tree is built (microtask: runs right after the
308
+ // current instantiation finishes, without yielding to render-loop frames)
309
+ Promise.resolve().then(() => Focus.set(this))
238
310
  }
239
311
 
240
312
  return Component('App', config)
@@ -21,6 +21,7 @@ import events from './events.js'
21
21
  import router from './router.js'
22
22
  import announcer from './announcer.js'
23
23
  import utils from './utils.js'
24
+ import settings from './settings.js'
24
25
  import symbols from '../../lib/symbols.js'
25
26
  import { reactive } from '../../lib/reactivity/reactive.js'
26
27
  import Settings from '../../settings.js'
@@ -29,6 +30,7 @@ export const shared = {
29
30
  ...events,
30
31
  ...router,
31
32
  ...announcer,
33
+ ...settings,
32
34
  $reactive: {
33
35
  value: function (target) {
34
36
  return reactive(target, Settings.get('reactivityMode'), true)
@@ -27,7 +27,7 @@ import { trigger } from '../../lib/reactivity/effect.js'
27
27
  import { Log } from '../../lib/log.js'
28
28
  import { removeEffects } from '../../lib/reactivity/effect.js'
29
29
  import { renderer } from '../../launch.js'
30
- import { keyMap } from '../../application.js'
30
+ import { keyMap } from '../../keymap.js'
31
31
  import { platform } from '../../platform.js'
32
32
 
33
33
  const selectCache = new WeakMap()
@@ -149,9 +149,9 @@ const createRouter = (component, routerViewName) => {
149
149
  toRouterView(routerView, path, data, options)
150
150
  return true
151
151
  },
152
- back() {
152
+ back(steps = 1) {
153
153
  const routerView = resolveRouterView(component, routerViewName)
154
- return routerView === null ? false : back.call(routerView)
154
+ return routerView === null ? false : back.call(routerView, steps)
155
155
  },
156
156
  get(name) {
157
157
  return getRouterFromCache(component, name)
@@ -172,6 +172,10 @@ const createRouter = (component, routerViewName) => {
172
172
  get navigating() {
173
173
  return state.navigating
174
174
  },
175
+ get history() {
176
+ const routerView = resolveRouterView(component, routerViewName)
177
+ return routerView === null ? [] : routerView.history.slice()
178
+ },
175
179
  state,
176
180
  }
177
181
  }
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright 2023 Comcast Cable Communications Management, LLC
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
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ import Settings from '../../settings.js'
19
+ import engine from '../../engine.js'
20
+ import { Log } from '../../lib/log.js'
21
+
22
+ // Settings that can be changed at runtime via `this.$settings.set()`.
23
+ const blitsKeys = [
24
+ 'keymap',
25
+ 'debugLevel',
26
+ 'inputThrottle',
27
+ 'announcerOptions',
28
+ 'enableMouse',
29
+ 'mouseMoveThrottle',
30
+ ]
31
+
32
+ const rendererKeys = [
33
+ 'w',
34
+ 'h',
35
+ 'screenResolution',
36
+ 'renderQuality',
37
+ 'pixelRatio',
38
+ 'canvasColor',
39
+ 'viewportMargin',
40
+ 'maxFPS',
41
+ 'fpsInterval',
42
+ ]
43
+
44
+ const changeableKeys = [...blitsKeys, ...rendererKeys]
45
+
46
+ export default {
47
+ $settings: {
48
+ value: {
49
+ get(key, defaultValue) {
50
+ return Settings.get(key, defaultValue)
51
+ },
52
+ set(key, value) {
53
+ if (changeableKeys.indexOf(key) === -1) {
54
+ Log.warn(`Setting "${key}" can not be changed at runtime`)
55
+ return
56
+ }
57
+ Settings.set(key, value)
58
+
59
+ if (rendererKeys.indexOf(key) !== -1) {
60
+ engine.updateSetting(key, value)
61
+ }
62
+ },
63
+ },
64
+ writable: false,
65
+ enumerable: true,
66
+ configurable: false,
67
+ },
68
+ }
@@ -18,6 +18,7 @@
18
18
  import { Log } from '../../lib/log.js'
19
19
 
20
20
  import symbols from '../../lib/symbols.js'
21
+ import { createComputed } from '../../lib/reactivity/effect.js'
21
22
 
22
23
  export default (component, computeds) => {
23
24
  component[symbols.computedKeys] = []
@@ -40,7 +41,10 @@ export default (component, computeds) => {
40
41
  component[symbols.computedKeys].push(computed)
41
42
  Object.defineProperty(component, computed, {
42
43
  get() {
43
- return computeds[computed].apply(this)
44
+ // Setup runs on the shared prototype; install the cache on first instance access.
45
+ const get = createComputed(computeds[computed], this, computed)
46
+ Object.defineProperty(this, computed, { get })
47
+ return get()
44
48
  },
45
49
  })
46
50
  }
package/src/component.js CHANGED
@@ -426,8 +426,9 @@ const Component = (name = required('name'), config = required('config')) => {
426
426
  }
427
427
  }
428
428
 
429
- // finaly set the lifecycle state to ready (in the next tick)
430
- setTimeout(() => (this[symbols.lifecycle].state = 'ready'))
429
+ // finally set the lifecycle state to ready (synchronously: instantiation,
430
+ // reactivity setup and template spawn are all complete at this point)
431
+ this[symbols.lifecycle].state = 'ready'
431
432
 
432
433
  // and return this
433
434
  return this
@@ -495,16 +496,24 @@ const Component = (name = required('name'), config = required('config')) => {
495
496
  base = setupComponent(Object.create(Base), config)
496
497
  }
497
498
 
498
- // one time code generation (only if precompilation is turned off)
499
- if (config.code === undefined) {
500
- Log.debug(`Generating code for ${name} component`)
501
- config.code = codegenerator.call(config, parser(config.template, name), devMode)
502
- }
499
+ // Direct factory calls also prepare runtime code before rendering.
500
+ if (factory.prepare !== undefined) factory.prepare()
503
501
 
504
502
  // create an instance of the component, using base as the prototype (which contains Base)
505
503
  return component.call(Object.create(base), options, parentEl, parentComponent, rootComponent)
506
504
  }
507
505
 
506
+ // Precompiled factories have no preparation step. Runtime factories compile once,
507
+ // explicitly before holder creation, then take the same path as precompiled ones.
508
+ if (config.code === undefined) {
509
+ factory.prepare = () => {
510
+ Log.debug(`Generating code for ${name} component`)
511
+ config.code = codegenerator.call(config, parser(config.template, name), devMode)
512
+ factory.needsInteractive = factory.needsInteractive || !!config.code.usesHoverState
513
+ factory.prepare = undefined
514
+ }
515
+ }
516
+
508
517
  // store the config on the factory, in order to access the config
509
518
  // during the code generation step
510
519
  factory[Symbol.for('config')] = config
@@ -515,6 +524,13 @@ const Component = (name = required('name'), config = required('config')) => {
515
524
  // To determine whether dynamic component is actual Blits component or not
516
525
  factory[symbols.isComponent] = true
517
526
 
527
+ // Pointer hit testing supports both hover and click-to-focus/input.
528
+ factory.needsInteractive =
529
+ (config.hooks && !!(config.hooks.hover || config.hooks.unhover)) ||
530
+ (config.input &&
531
+ (typeof config.input.enter === 'function' || typeof config.input.any === 'function')) ||
532
+ !!(config.code && config.code.usesHoverState)
533
+
518
534
  return factory
519
535
  }
520
536
 
@@ -66,7 +66,7 @@ export default () =>
66
66
  'content',
67
67
  'placement',
68
68
  'inspector-data',
69
- 'holder',
69
+ 'interactive',
70
70
  ]
71
71
  const elementConfigs = []
72
72
  const forloops = []
@@ -133,6 +133,7 @@ export default () =>
133
133
  },
134
134
  ],
135
135
  context: {},
136
+ usesHoverState: false,
136
137
  },
137
138
  props: { size: 40, bgColor: 'white' },
138
139
  computed: {
@@ -16,11 +16,15 @@
16
16
  */
17
17
 
18
18
  import Blits from '../../index.js'
19
- import { renderer } from '../launch.js'
20
19
  import fps_sprite from '../../public/assets/fps_sprite.base64.js'
21
20
 
22
21
  // exporting as named export to allow selective importing
23
22
  export const FPScounter = Blits.Component('FPScounter', {
23
+ props: {
24
+ active: true,
25
+ show: true,
26
+ alpha: 1,
27
+ },
24
28
  /* eslint-disable no-unused-vars */
25
29
  code: {
26
30
  render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
@@ -68,7 +72,7 @@ export const FPScounter = Blits.Component('FPScounter', {
68
72
  'content',
69
73
  'placement',
70
74
  'inspector-data',
71
- 'holder',
75
+ 'interactive',
72
76
  ]
73
77
  const elementConfigs = []
74
78
  const forloops = []
@@ -630,6 +634,7 @@ export const FPScounter = Blits.Component('FPScounter', {
630
634
  },
631
635
  ],
632
636
  context: {},
637
+ usesHoverState: false,
633
638
  },
634
639
  state() {
635
640
  return {
@@ -662,28 +667,34 @@ export const FPScounter = Blits.Component('FPScounter', {
662
667
  avgFps: '---',
663
668
  minFps: '---',
664
669
  maxFps: '---',
670
+
671
+ data: {
672
+ minFps: 10000,
673
+ maxFps: 0,
674
+ totalFps: 0,
675
+ fpsUpdateCounter: 0,
676
+ avgFps: 0,
677
+ },
665
678
  }
666
679
  },
667
680
  hooks: {
668
- ready() {
669
- let minFps = 10000
670
- let maxFps = 0
671
- let avgFps = 0
672
- let totalFps = 0
673
- let fpsUpdateCounter = 0
674
-
675
- renderer.on('fpsUpdate', (rM, { fps }) => {
676
- minFps = Math.min(fps, minFps)
677
- maxFps = Math.max(fps, maxFps)
678
- totalFps += fps
679
- fpsUpdateCounter++
680
- avgFps = Math.round(totalFps / fpsUpdateCounter)
681
-
682
- this.fps = fps.toString().padStart(3, '0')
683
- this.avgFps = avgFps.toString().padStart(3, '0')
684
- this.minFps = minFps.toString().padStart(3, '0')
685
- this.maxFps = maxFps.toString().padStart(3, '0')
686
- })
681
+ /**
682
+ * @param {number} fps
683
+ */
684
+ fpsUpdate(fps) {
685
+ // early return when not active or hidden
686
+ if (!this.active || !this.show || this.alpha === 0) return
687
+
688
+ this.data.minFps = Math.min(fps, this.data.minFps)
689
+ this.data.maxFps = Math.max(fps, this.data.maxFps)
690
+ this.data.totalFps += fps
691
+ this.data.fpsUpdateCounter++
692
+ this.data.avgFps = Math.round(this.data.totalFps / this.data.fpsUpdateCounter)
693
+
694
+ this.fps = fps.toString().padStart(3, '0')
695
+ this.avgFps = this.data.avgFps.toString().padStart(3, '0')
696
+ this.minFps = this.data.minFps.toString().padStart(3, '0')
697
+ this.maxFps = this.data.maxFps.toString().padStart(3, '0')
687
698
  },
688
699
  },
689
700
  })
@@ -78,7 +78,7 @@ export default () =>
78
78
  'content',
79
79
  'placement',
80
80
  'inspector-data',
81
- 'holder',
81
+ 'interactive',
82
82
  ]
83
83
  const elementConfigs = []
84
84
  const forloops = []
@@ -130,6 +130,7 @@ export default () =>
130
130
  },
131
131
  effects: [],
132
132
  context: {},
133
+ usesHoverState: false,
133
134
  },
134
135
  props: {
135
136
  name: '',
@@ -585,7 +585,7 @@ const propsTransformer = {
585
585
  this.props['data'] = v
586
586
  }
587
587
  },
588
- set holder(v) {
588
+ set interactive(v) {
589
589
  this.props['interactive'] = v
590
590
  },
591
591
  }
@@ -16,9 +16,47 @@
16
16
  */
17
17
 
18
18
  import Element from './element.js'
19
- import Launch from './launch.js'
19
+ import Launch, { renderer } from './launch.js'
20
+ import colors from '../../lib/colors/colors.js'
21
+ import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
22
+
23
+ /**
24
+ * Maps Blits setting keys to renderer runtime options
25
+ *
26
+ * @type {Record<string, (value: any) => Partial<import('@lightningjs/renderer').RendererRuntimeSettings>>}
27
+ */
28
+ const rendererOptions = {
29
+ // defaults to transparent (same as on launch) for empty or invalid colors
30
+ w: (v) => ({ appWidth: v }),
31
+ h: (v) => ({ appHeight: v }),
32
+ screenResolution: (v) => ({
33
+ deviceLogicalPixelRatio: typeof v === 'string' ? (SCREEN_RESOLUTIONS[v] ?? 1) : v,
34
+ }),
35
+ renderQuality: (v) => ({
36
+ devicePhysicalPixelRatio: RENDER_QUALITIES[v] ?? v ?? 1,
37
+ }),
38
+ canvasColor: (v) => ({ clearColor: Number(colors.normalize(v ?? '', '0x00000000')) }),
39
+ fpsInterval: (v) => ({ fpsUpdateInterval: v }),
40
+ pixelRatio: (v) => ({ deviceLogicalPixelRatio: v }),
41
+ viewportMargin: (v) => ({ boundsMargin: v }),
42
+ maxFPS: (v) => ({ targetFPS: v }),
43
+ }
44
+
45
+ /**
46
+ * Applies a runtime setting to the renderer via RendererMain.setOptions()
47
+ *
48
+ * @param {string} key - Blits setting key (validated by $settings)
49
+ * @param {any} value - New value
50
+ */
51
+ const updateSetting = (key, value) => {
52
+ const rendererInstance = renderer
53
+ if (rendererInstance && typeof rendererInstance.setOptions === 'function') {
54
+ rendererInstance.setOptions(rendererOptions[key](value))
55
+ }
56
+ }
20
57
 
21
58
  export default {
22
59
  Element,
23
60
  Launch,
61
+ updateSetting,
24
62
  }