@lightningjs/blits 2.10.0 → 2.12.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.12.0
4
+
5
+ _8 oct 2026_
6
+
7
+ - Added support for rich text using _bb-code_-style formatting
8
+ - Capped default number of webworkers at 2 for faster App startup time
9
+ - Added selective instantiation of _only_ the relevant text renders based on the registered fonts
10
+ - Bumped renderer to 3.6.0 with latest rich text enhancements
11
+
12
+ ## 2.11.0
13
+
14
+ _28 sep 2026_
15
+
16
+ - Improved `$select()` method by adding caching
17
+ - Upgraded several dependencies
18
+ - Bumped renderer to 3.5.1
19
+
3
20
  ## 2.10.0
4
21
 
5
22
  _21 sep 2026_
package/index.d.ts CHANGED
@@ -1224,8 +1224,9 @@ declare module '@lightningjs/blits' {
1224
1224
  * Maximum number of web workers to spin up simultaneously for offloading functionality such
1225
1225
  * as image loading to separate threads (when supported by the browser)
1226
1226
  *
1227
- * If not specified defaults to the number of logical processers available as reported by
1228
- * `navigator.hardwareConcurrency` (or 2 if `navigator.hardwareConcurrency` is not supported)
1227
+ * If not specified defaults to the number of logical processers available on the device, as reported by
1228
+ * `navigator.hardwareConcurrency`, with a maximum of 2. If `navigator.hardwareConcurrency` is
1229
+ * not supported the default value will be 2
1229
1230
  */
1230
1231
  webWorkersLimit?: number
1231
1232
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -78,25 +78,25 @@
78
78
  "@babel/plugin-syntax-import-assertions": "^7.28.6",
79
79
  "@eslint/eslintrc": "^3.3.4",
80
80
  "@eslint/js": "^9.39.3",
81
- "c8": "^10.1.3",
81
+ "c8": "^12.0.0",
82
82
  "eslint": "^9.39.3",
83
83
  "eslint-config-prettier": "^10.1.8",
84
84
  "eslint-plugin-prettier": "^5.5.4",
85
85
  "fast-glob": "^3.3.3",
86
- "global-jsdom": "^26.0.0",
87
- "globals": "^16.3.0",
86
+ "global-jsdom": "^30.0.0",
87
+ "globals": "^17.12.0",
88
88
  "husky": "^9.1.7",
89
- "jsdom": "^26.0.0",
89
+ "jsdom": "^30.1.0",
90
90
  "lint-staged": "^15.5.2",
91
91
  "prettier": "^3.6.2",
92
- "sinon": "^21.0.1",
92
+ "sinon": "^22.1.0",
93
93
  "tap-diff": "^0.1.1",
94
94
  "tap-filter": "^1.0.3",
95
95
  "tape": "^5.9.0"
96
96
  },
97
97
  "dependencies": {
98
- "@lightningjs/renderer": "3.5.0",
99
- "magic-string": "^0.30.21"
98
+ "@lightningjs/renderer": "^3.6.0",
99
+ "magic-string": "^1.4.1"
100
100
  },
101
101
  "repository": {
102
102
  "type": "git",
@@ -30,6 +30,8 @@ import { renderer } from '../../launch.js'
30
30
  import { keyMap } from '../../application.js'
31
31
  import { platform } from '../../platform.js'
32
32
 
33
+ const selectCache = new WeakMap()
34
+
33
35
  export default {
34
36
  $focus: {
35
37
  /**
@@ -103,6 +105,7 @@ export default {
103
105
  */
104
106
  value: function () {
105
107
  this.eol = true
108
+ selectCache.delete(this)
106
109
  removeKeyUpCallbacks(this)
107
110
  this[symbols.lifecycle].state = 'destroy'
108
111
 
@@ -197,26 +200,60 @@ export default {
197
200
  // early exit when component is marked as end of life
198
201
  if (this.eol === true) return
199
202
 
200
- let selected = null
201
- this[symbols.children].forEach((child) => {
203
+ let cache = selectCache.get(this)
204
+ if (cache !== undefined && cache.has(ref) === true) {
205
+ const selected = cache.get(ref)
206
+ if (selected !== undefined && selected.eol !== true) return selected
207
+ cache.delete(ref)
208
+ }
209
+
210
+ for (const child of this[symbols.children]) {
211
+ if (child === undefined || child === null) continue
202
212
  if (Array.isArray(child)) {
203
- child.forEach((c) => {
204
- if (c['ref'] === ref) selected = c
205
- })
213
+ const selected = child.find((c) => c['ref'] === ref)
214
+ if (selected !== undefined) {
215
+ if (cache === undefined) {
216
+ cache = new Map()
217
+ selectCache.set(this, cache)
218
+ }
219
+ cache.set(ref, selected)
220
+ return selected
221
+ }
206
222
  } else if (Object.getPrototypeOf(child) === Object.prototype) {
207
- Object.keys(child).forEach((k) => {
208
- if (child[k]['ref'] === ref) selected = child[k]
209
- })
223
+ const selected = Object.values(child).find((c) => c['ref'] === ref)
224
+ if (selected !== undefined) {
225
+ if (cache === undefined) {
226
+ cache = new Map()
227
+ selectCache.set(this, cache)
228
+ }
229
+ cache.set(ref, selected)
230
+ return selected
231
+ }
210
232
  } else {
211
- if (child['ref'] === ref) selected = child
233
+ if (child['ref'] === ref) {
234
+ if (cache === undefined) {
235
+ cache = new Map()
236
+ selectCache.set(this, cache)
237
+ }
238
+ cache.set(ref, child)
239
+ return child
240
+ }
212
241
  }
213
- })
214
- return selected
242
+ }
243
+ return null
215
244
  },
216
245
  writable: false,
217
246
  enumerable: true,
218
247
  configurable: false,
219
248
  },
249
+ [symbols.invalidateSelectCache]: {
250
+ value: function () {
251
+ selectCache.delete(this)
252
+ },
253
+ writable: false,
254
+ enumerable: false,
255
+ configurable: false,
256
+ },
220
257
  $trigger: {
221
258
  /**
222
259
  * @this {import('../../component').BlitsComponent}
@@ -26,6 +26,7 @@ export default () =>
26
26
  const validAttributes = [
27
27
  'parent',
28
28
  'rotation',
29
+ 'rich',
29
30
  'w',
30
31
  'h',
31
32
  'x',
@@ -28,6 +28,7 @@ export const FPScounter = Blits.Component('FPScounter', {
28
28
  const validAttributes = [
29
29
  'parent',
30
30
  'rotation',
31
+ 'rich',
31
32
  'w',
32
33
  'h',
33
34
  'x',
@@ -38,6 +38,7 @@ export default () =>
38
38
  const validAttributes = [
39
39
  'parent',
40
40
  'rotation',
41
+ 'rich',
41
42
  'w',
42
43
  'h',
43
44
  'x',
@@ -240,6 +240,9 @@ const propsTransformer = {
240
240
  set rotation(v) {
241
241
  this.props['rotation'] = v * (Math.PI / 180)
242
242
  },
243
+ set rich(v) {
244
+ this.props['richText'] = v
245
+ },
243
246
  set w(v) {
244
247
  const parsed = parsePercentage.call(this, v, 'w')
245
248
  this.props['w'] = parsed
@@ -17,23 +17,27 @@
17
17
 
18
18
  import Settings from '../../settings.js'
19
19
  import { renderer } from './launch.js'
20
-
21
- const fontTypeMapping = {
22
- sdf: 'sdf',
23
- msdf: 'sdf',
24
- canvas: 'canvas',
25
- web: 'canvas',
26
- }
20
+ import { resolveFontEngineType } from './fontTypes.js'
27
21
 
28
22
  export default () => {
29
23
  const stage = renderer.stage
24
+ const renderMode = Settings.get('renderMode', 'webgl')
30
25
 
31
26
  const fonts = Settings.get('fonts', [])
32
27
  for (let i = 0; i < fonts.length; i++) {
33
28
  const font = fonts[i]
34
- const type = fontTypeMapping[font.type] || 'sdf'
29
+ const type = resolveFontEngineType(font.type)
35
30
 
36
31
  if (type === 'sdf') {
32
+ // The SDF text renderer is not compatible with the Canvas core renderer
33
+ // (no `sdf` font handler is registered), so skip these fonts instead of
34
+ // throwing in `stage.loadFont`.
35
+ if (renderMode === 'canvas') {
36
+ console.warn(
37
+ `Blits: Skipping SDF font "${font.family}" because SDF fonts are not supported with renderMode "canvas".`
38
+ )
39
+ continue
40
+ }
37
41
  stage.loadFont('sdf', {
38
42
  fontFamily: font.family,
39
43
  atlasUrl: font.png || (font.file && font.file.replace(/\.[^.]+$/, `.${font.type}.png`)),
@@ -0,0 +1,62 @@
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
+ /**
19
+ * Maps a Blits font `type` (as declared in `settings.fonts`) to the
20
+ * renderer font-engine type (`sdf` or `canvas`).
21
+ *
22
+ * Kept in a shared module so `launch.js` (font-engine selection) and
23
+ * `fontLoader.js` (font loading) always agree on the mapping.
24
+ */
25
+ const fontTypeMapping = {
26
+ sdf: 'sdf',
27
+ msdf: 'sdf',
28
+ canvas: 'canvas',
29
+ web: 'canvas',
30
+ }
31
+
32
+ /**
33
+ * Resolve a Blits font type to a renderer font-engine type.
34
+ * Unknown (or missing) types default to `sdf`, matching fontLoader behavior.
35
+ *
36
+ * @param {string} [type] - The font type declared in settings (web, msdf, sdf)
37
+ * @returns {'sdf'|'canvas'}
38
+ */
39
+ export const resolveFontEngineType = (type) => fontTypeMapping[type] || 'sdf'
40
+
41
+ /**
42
+ * Scan the configured fonts and report which font-engine types are required.
43
+ *
44
+ * @param {Array<{ type?: string }>} [fonts] - Fonts declared in settings
45
+ * @returns {{ hasSdfFont: boolean, hasCanvasFont: boolean }}
46
+ */
47
+ export const getRequiredFontEngines = (fonts = []) => {
48
+ let hasSdfFont = false
49
+ let hasCanvasFont = false
50
+
51
+ const len = Array.isArray(fonts) ? fonts.length : 0
52
+ for (let i = 0; i < len; i++) {
53
+ if (resolveFontEngineType(fonts[i] && fonts[i].type) === 'sdf') {
54
+ hasSdfFont = true
55
+ } else {
56
+ hasCanvasFont = true
57
+ }
58
+ if (hasSdfFont === true && hasCanvasFont === true) break
59
+ }
60
+
61
+ return { hasSdfFont, hasCanvasFont }
62
+ }
@@ -25,6 +25,7 @@ import { SCREEN_RESOLUTIONS, RENDER_QUALITIES } from '../../constants.js'
25
25
  import colors from '../../lib/colors/colors.js'
26
26
  import fontLoader from './fontLoader.js'
27
27
  import shaderLoader from './shaderLoader.js'
28
+ import { getRequiredFontEngines } from './fontTypes.js'
28
29
  import { platform } from '../../platform.js'
29
30
 
30
31
  /** @type {RendererMain|{}} */
@@ -37,11 +38,22 @@ const renderEngine = (settings) => {
37
38
  if (renderMode === 'canvas') return CanvasCoreRenderer
38
39
  }
39
40
 
40
- const textRenderEngines = (settings) => {
41
+ export const textRenderEngines = (settings) => {
41
42
  const renderMode = 'renderMode' in settings ? settings.renderMode : 'webgl'
42
43
 
43
- if (renderMode === 'webgl') return [SdfTextRenderer, CanvasTextRenderer]
44
+ // SDF text rendering is not compatible with the Canvas core renderer
44
45
  if (renderMode === 'canvas') return [CanvasTextRenderer]
46
+
47
+ if (renderMode === 'webgl') {
48
+ // Only register the font engines actually required by the configured
49
+ // fonts, so unused text renderers are never initialized.
50
+ const { hasSdfFont, hasCanvasFont } = getRequiredFontEngines(settings.fonts)
51
+ if (hasSdfFont === true && hasCanvasFont === true) return [SdfTextRenderer, CanvasTextRenderer]
52
+ if (hasSdfFont === true) return [SdfTextRenderer]
53
+ // Canvas-only fonts, or no fonts declared at all (system/default fonts
54
+ // render via the Canvas engine, which can render any font family).
55
+ return [CanvasTextRenderer]
56
+ }
45
57
  }
46
58
 
47
59
  const textureMemorySettings = (settings) => {
@@ -93,7 +105,9 @@ export default (App, target, settings = {}) => {
93
105
  SCREEN_RESOLUTIONS[screenHeight] ||
94
106
  1,
95
107
  numImageWorkers:
96
- 'webWorkersLimit' in settings ? settings.webWorkersLimit : hardwareConcurrency || 2,
108
+ 'webWorkersLimit' in settings
109
+ ? settings.webWorkersLimit
110
+ : Math.min(hardwareConcurrency || 2, 2),
97
111
  clearColor: (settings.canvasColor && colors.normalize(settings.canvasColor)) || 0x00000000,
98
112
  inspector: settings.inspector === true ? Inspector : undefined,
99
113
  boundsMargin: settings.viewportMargin || 0,
@@ -223,6 +223,9 @@ const generateElementCode = function (
223
223
 
224
224
  if (key === 'key') return
225
225
 
226
+ // a ref on a component belongs to the component itself, not to its holder element
227
+ if (options.holder === true && (key === 'ref' || key === ':ref')) return
228
+
226
229
  // Skip inspector-data in production builds for performance optimization
227
230
  if (key === 'inspector-data' && !isDev) return
228
231
 
@@ -524,6 +527,8 @@ const generateForLoopCode = function (templateObject, parent) {
524
527
  }
525
528
  }
526
529
 
530
+ const invalidateSelectCache = 'ref' in templateObject
531
+
527
532
  ctx.renderCode.push(`
528
533
  created[${forStartCounter}] = []
529
534
  effects[${forStartCounter}] = []
@@ -532,6 +537,7 @@ const generateForLoopCode = function (templateObject, parent) {
532
537
  let to${forStartCounter}
533
538
 
534
539
  forloops[${forStartCounter}] = (collection = [], elms, created) => {
540
+ ${invalidateSelectCache ? "rootComponent && rootComponent[Symbol.for('invalidateSelectCache')]()" : ''}
535
541
  const rawCollection = getRaw(collection)
536
542
  const keys = new Set()
537
543
  let l = rawCollection.length
@@ -144,4 +144,6 @@ export default {
144
144
  removeEffects: Symbol.for('removeEffects'),
145
145
  // Symbol 'tagContent' utilized within generated code
146
146
  tagContent: Symbol.for('tagContent'),
147
+ // Symbol used by generated loops to invalidate cached ref lookups
148
+ invalidateSelectCache: Symbol.for('invalidateSelectCache'),
147
149
  }