@lightningjs/blits 2.4.1 → 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
+ }
@@ -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'
@@ -1,256 +0,0 @@
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 Component from '../component.js'
19
-
20
- import symbols from '../lib/symbols.js'
21
-
22
- export default () =>
23
- Component('Sprite', {
24
- /* eslint-disable no-unused-vars */
25
- code: {
26
- render: function anonymous(parent, component, context, components, effect, getRaw, Log) {
27
- const elms = []
28
- const validAttributes = [
29
- 'parent',
30
- 'rotation',
31
- 'w',
32
- 'h',
33
- 'x',
34
- 'y',
35
- 'z',
36
- 'zIndex',
37
- 'color',
38
- 'src',
39
- 'texture',
40
- 'fit',
41
- 'rtt',
42
- 'mount',
43
- 'pivot',
44
- 'scale',
45
- 'show',
46
- 'alpha',
47
- 'rounded',
48
- 'border',
49
- 'shadow',
50
- 'shader',
51
- 'clipping',
52
- 'overflow',
53
- 'font',
54
- 'size',
55
- 'maxwidth',
56
- 'maxheight',
57
- 'maxlines',
58
- 'textoverflow',
59
- 'letterspacing',
60
- 'lineheight',
61
- 'contain',
62
- 'align',
63
- 'content',
64
- 'placement',
65
- 'inspector-data',
66
- 'holder',
67
- ]
68
- const elementConfigs = []
69
- const forloops = []
70
- const props = []
71
- const created = []
72
- const effects = {}
73
- const skips = []
74
- let componentType
75
- let rootComponent = component
76
- let propData
77
- let slotComponent
78
- let inSlot = false
79
- let slotChildCounter = 0
80
- let cmps = []
81
- elementConfigs[0] = {}
82
- elementConfigs[0]['___wrapper'] = true
83
-
84
- elms[0] = this.element(
85
- { parent: parent || 'root' },
86
- inSlot === true ? slotComponent : component
87
- )
88
-
89
- elementConfigs[0]['w'] = parent.node.w * (100 / 100)
90
- elementConfigs[0]['h'] = parent.node.h * (100 / 100)
91
- elementConfigs[0]['texture'] = component.texture
92
- elementConfigs[0]['color'] = component.color
93
- elementConfigs[0]['rounded'] = component.rounded
94
- elementConfigs[0]['border'] = component.border
95
- elementConfigs[0]['shadow'] = component.shadow
96
- elms[0].populate(elementConfigs[0])
97
-
98
- if (inSlot === true) {
99
- slotChildCounter -= 1
100
- }
101
-
102
- return {
103
- elms,
104
- cleanup: () => {
105
- rootComponent = null
106
- propData = null
107
- slotComponent = null
108
- parent = null
109
- component = null
110
- cmps.length = 0
111
- elms.length = 0
112
- components.length = 0
113
- elementConfigs.length = 0
114
- forloops.length = 0
115
- props.length = 0
116
- skips.length = 0
117
- },
118
- skips,
119
- }
120
- },
121
- effects: [
122
- function anonymous(component, elms, context, components, rootComponent, skips, effect) {
123
- elms[0].set('texture', component.texture)
124
- },
125
- function anonymous(component, elms, context, components, rootComponent, skips, effect) {
126
- elms[0].set('color', component.color)
127
- },
128
- function anonymous(component, elms, context, components, rootComponent, skips, effect) {
129
- elms[0].set('rounded', component.rounded)
130
- },
131
- function anonymous(component, elms, context, components, rootComponent, skips, effect) {
132
- elms[0].set('border', component.border)
133
- },
134
- function anonymous(component, elms, context, components, rootComponent, skips, effect) {
135
- elms[0].set('shadow', component.shadow)
136
- },
137
- ],
138
- context: {},
139
- },
140
- props: {
141
- image: undefined,
142
- map: undefined,
143
- frame: undefined,
144
- color: undefined,
145
- rounded: undefined,
146
- border: undefined,
147
- shadow: undefined,
148
- '@loaded': undefined,
149
- '@error': undefined,
150
- },
151
- state() {
152
- return {
153
- spriteTexture: null,
154
- currentSrc: null,
155
- }
156
- },
157
-
158
- hooks: {
159
- ready() {
160
- const loaded = this['@loaded']
161
- if (loaded && typeof loaded === 'function') {
162
- const cb = (payload) => loaded({ w: payload?.w, h: payload?.h }, this[symbols.wrapper])
163
- this._loadedCb = cb
164
- if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
165
- this.spriteTexture.on('loaded', cb)
166
- }
167
- }
168
- const error = this['@error']
169
- if (error && typeof error === 'function') {
170
- const cb = (payload) => error(payload, this[symbols.wrapper])
171
- this._failedCb = cb
172
- if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
173
- this.spriteTexture.on('failed', cb)
174
- }
175
- }
176
- },
177
- destroy() {
178
- if (this.spriteTexture !== undefined && this.spriteTexture !== null) {
179
- if (this._loadedCb) this.spriteTexture.off('loaded', this._loadedCb)
180
- if (this._failedCb) this.spriteTexture.off('failed', this._failedCb)
181
- }
182
- },
183
- },
184
-
185
- computed: {
186
- texture() {
187
- // If no image - nothing to render
188
- if (this.image === undefined || this.image === null) {
189
- return null
190
- }
191
-
192
- // Get renderer
193
- const renderer = this[symbols.renderer]()
194
- if (renderer === null || renderer === undefined || renderer.createTexture === undefined) {
195
- return null
196
- }
197
-
198
- // Recreate texture only when image src changes
199
- if (
200
- this.spriteTexture === undefined ||
201
- this.spriteTexture === null ||
202
- this.currentSrc !== this.image
203
- ) {
204
- const prevTexture = this.spriteTexture
205
- if (prevTexture !== undefined && prevTexture !== null) {
206
- if (this._loadedCb) prevTexture.off('loaded', this._loadedCb)
207
- if (this._failedCb) prevTexture.off('failed', this._failedCb)
208
- }
209
- this.spriteTexture = renderer.createTexture('ImageTexture', {
210
- src: this.image,
211
- })
212
- this.currentSrc = this.image
213
- if (this._loadedCb) this.spriteTexture.on('loaded', this._loadedCb)
214
- if (this._failedCb) this.spriteTexture.on('failed', this._failedCb)
215
- }
216
-
217
- // Resolve frame data from sprite map
218
- let options = null
219
- if (
220
- this.map !== null &&
221
- this.map !== undefined &&
222
- this.frame !== null &&
223
- this.frame !== undefined
224
- ) {
225
- if (
226
- this.map.frames !== null &&
227
- this.map.frames !== undefined &&
228
- this.frame in this.map.frames
229
- ) {
230
- options = Object.assign({}, this.map.defaults || {}, this.map.frames[this.frame])
231
- } else if (this.frame in this.map) {
232
- options = this.map[this.frame]
233
- }
234
- }
235
-
236
- // If no map but frame is object (manual subtexture)
237
- if ((options === null || options === undefined) && typeof this.frame === 'object') {
238
- options = this.frame
239
- }
240
-
241
- // Create SubTexture only if frame data exists
242
- if (options !== null && options !== undefined) {
243
- return renderer.createTexture('SubTexture', {
244
- texture: this.spriteTexture,
245
- x: options.x,
246
- y: options.y,
247
- w: options.w,
248
- h: options.h,
249
- })
250
- }
251
-
252
- // Default fallback (single image)
253
- return this.spriteTexture
254
- },
255
- },
256
- })