@lightningjs/blits 2.4.0 → 2.5.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.
@@ -46,6 +46,9 @@ export default () =>
46
46
  'color',
47
47
  'src',
48
48
  'texture',
49
+ 'image',
50
+ 'map',
51
+ 'frame',
49
52
  'fit',
50
53
  'rtt',
51
54
  'mount',
@@ -17,10 +17,8 @@
17
17
 
18
18
  import Circle from './Circle.js'
19
19
  import RouterView from './RouterView.js'
20
- import Sprite from './Sprite.js'
21
20
 
22
21
  export default () => ({
23
22
  Circle: Circle(),
24
23
  RouterView: RouterView(),
25
- Sprite: Sprite(),
26
24
  })
@@ -29,9 +29,14 @@ import { Log } from '../../lib/log.js'
29
29
  import symbols from '../../lib/symbols.js'
30
30
  import Settings from '../../settings.js'
31
31
  import shaders from '../../lib/shaders/shaders.js'
32
+ import { resolveSpriteTexture } from './spriteTexture.js'
32
33
 
33
34
  const holderComponentMap = new WeakMap()
34
35
 
36
+ /** @param {any} frame */
37
+ const spriteFrameKey = (frame) =>
38
+ typeof frame === 'object' && frame !== null ? JSON.stringify(frame) : frame
39
+
35
40
  /**
36
41
  * Creates a padding object from a value and direction.
37
42
  * @param {number|object|string|undefined} padding - The padding value.
@@ -291,6 +296,24 @@ const propsTransformer = {
291
296
  this.props['color'] = 0xffffffff
292
297
  }
293
298
  },
299
+ set image(v) {
300
+ this.raw['image'] = v
301
+ if (this.element[symbols.isSprite] === true && this.element.node) {
302
+ this.element._scheduleNativeSpriteSync()
303
+ }
304
+ },
305
+ set map(v) {
306
+ this.raw['map'] = v
307
+ if (this.element[symbols.isSprite] === true && this.element.node) {
308
+ this.element._scheduleNativeSpriteSync()
309
+ }
310
+ },
311
+ set frame(v) {
312
+ this.raw['frame'] = v
313
+ if (this.element[symbols.isSprite] === true && this.element.node) {
314
+ this.element._scheduleNativeSpriteSync()
315
+ }
316
+ },
294
317
  set fit(v) {
295
318
  const resizeMode = {}
296
319
 
@@ -548,6 +571,7 @@ export const elementAttributes = Object.keys(propsTransformer)
548
571
  const Element = {
549
572
  /**
550
573
  * Populates the element with data
574
+ * @this {import('../../component.js').BlitsElement}
551
575
  * @param {import('../../component.js').BlitsElementProps} props
552
576
  */
553
577
  populate(props) {
@@ -557,6 +581,21 @@ const Element = {
557
581
  this[symbols.isSlot] = true
558
582
  }
559
583
 
584
+ if (props[symbols.isSprite] === true) {
585
+ this[symbols.isSprite] = true
586
+ this._spriteState = {
587
+ spriteTexture: null,
588
+ currentSrc: null,
589
+ _loadedCb: null,
590
+ _failedCb: null,
591
+ _syncScheduled: false,
592
+ lastTexture: null,
593
+ lastImage: null,
594
+ lastMap: null,
595
+ lastFrameKey: undefined,
596
+ }
597
+ }
598
+
560
599
  this.props.element = this
561
600
 
562
601
  this.props['parent'] = props['parent'] || this.config.parent
@@ -600,16 +639,20 @@ const Element = {
600
639
  holderComponentMap.set(this.node, this.component)
601
640
  }
602
641
 
603
- if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
604
- this.node.on('loaded', (el, { type, dimensions }) => {
605
- props['@loaded']({ w: dimensions.w, h: dimensions.h, type }, this)
606
- })
607
- }
642
+ if (this[symbols.isSprite] === true) {
643
+ this._syncNativeSprite()
644
+ } else {
645
+ if (props['@loaded'] !== undefined && typeof props['@loaded'] === 'function') {
646
+ this.node.on('loaded', (el, { type, dimensions }) => {
647
+ props['@loaded']({ w: dimensions.w, h: dimensions.h, type }, this)
648
+ })
649
+ }
608
650
 
609
- if (props['@error'] !== undefined && typeof props['@error'] === 'function') {
610
- this.node.on('failed', (el, error) => {
611
- props['@error'](error, this)
612
- })
651
+ if (props['@error'] !== undefined && typeof props['@error'] === 'function') {
652
+ this.node.on('failed', (el, error) => {
653
+ props['@error'](error, this)
654
+ })
655
+ }
613
656
  }
614
657
 
615
658
  if (props.__layout === true) {
@@ -657,6 +700,78 @@ const Element = {
657
700
  this.node.data = { ...this.props.props['data'] }
658
701
  }
659
702
  },
703
+ /**
704
+ * @this {import('../../component.js').BlitsElement}
705
+ */
706
+ _scheduleNativeSpriteSync() {
707
+ if (this[symbols.isSprite] !== true || !this.node || this.eol === true) return
708
+ const st = this._spriteState
709
+ if (st === undefined) return
710
+ if (st._syncScheduled === true) return
711
+ st._syncScheduled = true
712
+ queueMicrotask(() => {
713
+ st._syncScheduled = false
714
+ if (this.eol === true || !this.node) return
715
+ this._syncNativeSprite()
716
+ })
717
+ },
718
+ /**
719
+ * @this {import('../../component.js').BlitsElement}
720
+ */
721
+ _syncNativeSprite() {
722
+ if (this[symbols.isSprite] !== true || !this.node) return
723
+ const st = this._spriteState
724
+ if (st === undefined) return
725
+
726
+ const raw = this.props.raw
727
+ const image = raw['image']
728
+ const map = raw['map']
729
+ const frameKey = spriteFrameKey(raw['frame'])
730
+
731
+ if (
732
+ st.lastTexture != null &&
733
+ image === st.lastImage &&
734
+ map === st.lastMap &&
735
+ frameKey === st.lastFrameKey
736
+ ) {
737
+ return
738
+ }
739
+
740
+ const imageChanged = image !== st.lastImage
741
+ st.lastImage = image
742
+ st.lastMap = map
743
+ st.lastFrameKey = frameKey
744
+
745
+ const texture = resolveSpriteTexture(this, renderer, st)
746
+ if (texture === st.lastTexture) return
747
+
748
+ st.lastTexture = texture
749
+ this.set('texture', texture)
750
+
751
+ if (imageChanged !== true) return
752
+
753
+ const prevL = st._loadedCb
754
+ const prevF = st._failedCb
755
+ st._loadedCb =
756
+ raw['@loaded'] && typeof raw['@loaded'] === 'function'
757
+ ? (payload) => {
758
+ const d = payload?.dimensions
759
+ const w = payload?.w ?? payload?.width ?? d?.width ?? d?.w
760
+ const h = payload?.h ?? payload?.height ?? d?.height ?? d?.h
761
+ raw['@loaded']({ w, h }, this)
762
+ }
763
+ : null
764
+ st._failedCb =
765
+ raw['@error'] && typeof raw['@error'] === 'function'
766
+ ? (payload) => raw['@error'](payload, this)
767
+ : null
768
+ if (st.spriteTexture) {
769
+ if (prevL) st.spriteTexture.off('loaded', prevL)
770
+ if (prevF) st.spriteTexture.off('failed', prevF)
771
+ if (st._loadedCb) st.spriteTexture.on('loaded', st._loadedCb)
772
+ if (st._failedCb) st.spriteTexture.on('failed', st._failedCb)
773
+ }
774
+ },
660
775
  /**
661
776
  * Set an individual property on the node
662
777
  *
@@ -823,6 +938,16 @@ const Element = {
823
938
 
824
939
  Log.debug('Deleting Node', this.nodeId)
825
940
 
941
+ if (this[symbols.isSprite] === true && this._spriteState) {
942
+ const st = this._spriteState
943
+ if (st.spriteTexture) {
944
+ if (st._loadedCb) st.spriteTexture.off('loaded', st._loadedCb)
945
+ if (st._failedCb) st.spriteTexture.off('failed', st._failedCb)
946
+ }
947
+ if (st.subTextures) st.subTextures.clear()
948
+ this._spriteState = null
949
+ }
950
+
826
951
  // Clear all pending debounce timeouts
827
952
  const debounceProps = Object.keys(this.debounceTimeouts)
828
953
  for (let i = 0; i < debounceProps.length; i++) {
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Resolves a sprite sheet frame from map + frame (template Sprite element path on L3).
3
+ * @param {import('../../component.js').BlitsElement} element
4
+ * @param {object} r - renderer
5
+ * @param {any} state - mutated sprite cache (see L3 element _spriteState)
6
+ * @returns {any|null} texture for the node
7
+ */
8
+ export function resolveSpriteTexture(element, r, state) {
9
+ const image = element.props.raw['image']
10
+ if (image === undefined || image === null) {
11
+ return null
12
+ }
13
+ if (r === null || r === undefined || r.createTexture === undefined) {
14
+ return null
15
+ }
16
+
17
+ if (
18
+ state.spriteTexture === undefined ||
19
+ state.spriteTexture === null ||
20
+ state.currentSrc !== image
21
+ ) {
22
+ state.spriteTexture = r.createTexture('ImageTexture', {
23
+ src: image,
24
+ })
25
+ state.currentSrc = image
26
+ state.subTextures = null
27
+ state.currentMap = null
28
+ }
29
+
30
+ const map = element.props.raw['map']
31
+
32
+ if (state.currentMap !== map) {
33
+ state.currentMap = map
34
+ state.subTextures = null
35
+ }
36
+ const frame = element.props.raw['frame']
37
+
38
+ let options = null
39
+ if (map != null && frame != null) {
40
+ if (map.frames != null && map.frames !== undefined && frame in map.frames) {
41
+ options = Object.assign({}, map.defaults || {}, map.frames[frame])
42
+ } else if (frame in map) {
43
+ options = map[frame]
44
+ }
45
+ }
46
+
47
+ if ((options === null || options === undefined) && typeof frame === 'object' && frame !== null) {
48
+ options = frame
49
+ }
50
+
51
+ if (options != null) {
52
+ const key = typeof frame === 'object' ? JSON.stringify(frame) : String(frame)
53
+ if (!state.subTextures) {
54
+ state.subTextures = new Map()
55
+ }
56
+ const cached = state.subTextures.get(key)
57
+ if (cached !== undefined) {
58
+ return cached
59
+ }
60
+ const subTexture = r.createTexture('SubTexture', {
61
+ texture: state.spriteTexture,
62
+ x: options.x,
63
+ y: options.y,
64
+ w: options.w,
65
+ h: options.h,
66
+ })
67
+ state.subTextures.set(key, subTexture)
68
+ return subTexture
69
+ }
70
+ return state.spriteTexture
71
+ }
@@ -41,6 +41,8 @@ export default {
41
41
  return focusedComponent
42
42
  },
43
43
  set(component, event) {
44
+ if (component === undefined || component.eol === true) return
45
+
44
46
  clearTimeout(setFocusTimeout)
45
47
 
46
48
  // early return if already focused
@@ -75,7 +77,7 @@ export default {
75
77
 
76
78
  // and finally set focus to the leaf component
77
79
  setFocusTimeout = setTimeout(
78
- () => setFocus(component, event),
80
+ () => component.eol !== true && setFocus(component, event),
79
81
  this.hold === true ? Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS) : 0
80
82
  )
81
83
  },
@@ -206,6 +206,10 @@ const generateElementCode = function (
206
206
  renderCode.push(`elementConfigs[${counter}][Symbol.for('isSlot')] = true`)
207
207
  }
208
208
 
209
+ if (templateObject[Symbol.for('componentType')] === 'Sprite') {
210
+ renderCode.push(`elementConfigs[${counter}][Symbol.for('isSprite')] = true`)
211
+ }
212
+
209
213
  if (options.holder) {
210
214
  renderCode.push(`elementConfigs[${counter}]['holder'] = true`)
211
215
  }
@@ -611,11 +615,16 @@ const generateForLoopCode = function (templateObject, parent) {
611
615
  if (
612
616
  templateObject[Symbol.for('componentType')] === 'Element' ||
613
617
  templateObject[Symbol.for('componentType')] === 'Slot' ||
614
- templateObject[Symbol.for('componentType')] === 'Text'
618
+ templateObject[Symbol.for('componentType')] === 'Text' ||
619
+ templateObject[Symbol.for('componentType')] === 'Sprite' ||
620
+ templateObject[Symbol.for('componentType')] === 'Layout'
615
621
  ) {
616
622
  if (templateObject[Symbol.for('componentType')] === 'Text') {
617
623
  templateObject.__textnode = 'true'
618
624
  }
625
+ if (templateObject[Symbol.for('componentType')] === 'Layout') {
626
+ templateObject.__layout = 'true'
627
+ }
619
628
  generateElementCode.call(ctx, templateObject, parent, {
620
629
  key: 'scope.key',
621
630
  component: 'scope.',
@@ -788,7 +797,8 @@ const generateCode = function (templateObject, parent = false, options = {}) {
788
797
  childTemplateObject[Symbol.for('componentType')] === 'Element' ||
789
798
  childTemplateObject[Symbol.for('componentType')] === 'Slot' ||
790
799
  childTemplateObject[Symbol.for('componentType')] === 'Text' ||
791
- childTemplateObject[Symbol.for('componentType')] === 'Layout'
800
+ childTemplateObject[Symbol.for('componentType')] === 'Layout' ||
801
+ childTemplateObject[Symbol.for('componentType')] === 'Sprite'
792
802
  ) {
793
803
  if (childTemplateObject[Symbol.for('componentType')] === 'Text') {
794
804
  childTemplateObject.__textnode = 'true'
@@ -61,6 +61,7 @@
61
61
  * @property {symbol} components
62
62
  * @property {symbol} config
63
63
  * @property {symbol} isSlot
64
+ * @property {symbol} isSprite
64
65
  * @property {symbol} props
65
66
  * @property {symbol} parent
66
67
  * @property {symbol} slots
@@ -68,6 +69,7 @@
68
69
  * @property {symbol} isComponent
69
70
  * @property {symbol} effects
70
71
  * @property {symbol} removeGlobalEffects
72
+ * @property {symbol} tagContent
71
73
  */
72
74
 
73
75
  /**
@@ -126,6 +128,8 @@ export default {
126
128
  config: Symbol.for('config'),
127
129
  // Symbol 'isSlot' utilized within generated code
128
130
  isSlot: Symbol.for('isSlot'),
131
+ // Symbol 'isSprite' utilized within generated code (native Sprite on L3 element)
132
+ isSprite: Symbol.for('isSprite'),
129
133
  // Symbol 'props' utilized within generated code
130
134
  props: Symbol.for('props'),
131
135
  // Symbol 'slots' utilized within generated code
@@ -0,0 +1 @@
1
+ export { WebPlatform, WebPlatformNext, WebPlatformChrome50, WebPlatformLegacy } from '@lightningjs/renderer/platforms'
@@ -0,0 +1,23 @@
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
+ export {
19
+ WebPlatform,
20
+ WebPlatformNext,
21
+ WebPlatformChrome50,
22
+ WebPlatformLegacy,
23
+ } from '@lightningjs/renderer/platforms'
@@ -408,33 +408,32 @@ const executeBeforeHook = async function (
408
408
  try {
409
409
  result = await hooks[hookName].call(parent, route, previousRoute)
410
410
  if (isString(result)) {
411
- currentRoute = previousRoute
412
- to(result)
411
+ this.currentRoute = previousRoute
412
+ to(result, {}, {}, this.name)
413
413
  return false
414
414
  }
415
415
  } catch (error) {
416
416
  Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
417
+ this.currentRoute = previousRoute
417
418
  if (this.history.length > 0) {
418
419
  preventHashChangeNavigation = true
419
- currentRoute = previousRoute
420
- /// hmmmm
421
420
  window.history.back()
422
421
  navigatingBack = false
423
422
  state.navigating = false
424
- return false
425
423
  }
424
+ return false
426
425
  }
427
426
  // If the resolved result is an object, redirect if the path in the object was changed
428
427
  if (isObject(result) === true && result.path !== currentPath) {
429
- currentRoute = previousRoute
430
- to(result.path, result.data, result.options)
428
+ this.currentRoute = previousRoute
429
+ to(result.path, result.data, result.options, this.name)
431
430
  return false
432
431
  }
433
432
  // If the resolved result is false, cancel navigation
434
433
  if (result === false) {
434
+ this.currentRoute = previousRoute
435
435
  if (this.history.length > 0) {
436
436
  preventHashChangeNavigation = true
437
- currentRoute = previousRoute
438
437
  window.history.back()
439
438
  navigatingBack = false
440
439
  state.navigating = false
@@ -491,11 +490,11 @@ const executeTransition = async (transition, element, animate) => {
491
490
  }
492
491
  }
493
492
 
494
- export const to = (path, data = {}, options = {}) => {
493
+ export const to = (path, data = {}, options = {}, routerViewName = '') => {
495
494
  navigationData = data
496
495
  overrideOptions = options
497
496
 
498
- setHash(path)
497
+ setHash(path, routerViewName)
499
498
  }
500
499
 
501
500
  export const toRouterView = (routerView, path, data = {}, options = {}) => {