@lightningjs/blits 0.9.9 → 1.0.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/boilerplate/common/public/fonts/Lato-Regular.ttf +0 -0
  3. package/boilerplate/common/public/fonts/Raleway-ExtraBold.ttf +0 -0
  4. package/docs/essentials/displaying_text.md +1 -1
  5. package/package.json +3 -4
  6. package/src/component/base/events.js +1 -1
  7. package/src/component/base/utils.js +34 -1
  8. package/src/component/setup/props.js +3 -1
  9. package/src/component/setup/props.test.js +51 -19
  10. package/src/component.d.ts +2 -1
  11. package/src/component.js +20 -13
  12. package/src/components/Circle.js +13 -12
  13. package/src/components/FPScounter.js +413 -465
  14. package/src/components/Image.js +7 -9
  15. package/src/components/Layout.js +49 -0
  16. package/src/components/Layout.orig.js +34 -0
  17. package/src/components/RouterView.js +6 -10
  18. package/src/components/Sprite.js +10 -12
  19. package/src/components/index.js +2 -2
  20. package/src/components/index.orig.js +2 -2
  21. package/src/engine.js +1 -1
  22. package/src/engines/L3/element.js +193 -180
  23. package/src/engines/L3/element.test.js +170 -0
  24. package/src/engines/L3/fontLoader.js +20 -7
  25. package/src/engines/L3/index.js +2 -2
  26. package/src/engines/L3/launch.js +7 -13
  27. package/src/engines/L3/shaderLoader.js +12 -0
  28. package/src/engines/index.js +1 -1
  29. package/src/launch.d.ts +19 -2
  30. package/src/lib/codegenerator/generator.js +214 -77
  31. package/src/lib/codegenerator/generator.test.js +657 -942
  32. package/src/lib/colors/colors.js +59 -46
  33. package/src/lib/precompiler/precompiler.js +2 -1
  34. package/src/lib/reactivity/effect.js +10 -13
  35. package/src/lib/reactivity/reactive.js +24 -14
  36. package/src/lib/symbols.js +1 -0
  37. package/src/router/router.js +17 -3
  38. package/vite/msdfGenerator.js +52 -53
  39. package/boilerplate/common/public/fonts/Lato-Regular.msdf.json +0 -5383
  40. package/boilerplate/common/public/fonts/Lato-Regular.msdf.png +0 -0
  41. package/boilerplate/common/public/fonts/Raleway-ExtraBold.msdf.json +0 -10838
  42. package/boilerplate/common/public/fonts/Raleway-ExtraBold.msdf.png +0 -0
  43. package/boilerplate/js/default/src/renderer.js +0 -4
  44. package/src/components/Text.js +0 -119
  45. package/src/components/Text.orig.js +0 -81
@@ -15,64 +15,77 @@
15
15
  * SPDX-License-Identifier: Apache-2.0
16
16
  */
17
17
 
18
- import htmlColors from './htmlColors.js'
19
- import { Log } from '../log.js'
18
+ import colorsMap from './htmlColors.js'
19
+
20
+ const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i
21
+ const rgba =
22
+ /^(rgba?)\((\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*),(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*),(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*)(,(\s*(-?\d+(?:\.\d+)?)\s*))?\)$/
23
+ const hsla =
24
+ /^(hsla?)\((\s*(360|3[0-5][0-9]|[12]?[0-9]{1,2})\s*),(\s*(100|[1-9]?[0-9])\s*)%,(\s*(100|[1-9]?[0-9])\s*)%(,(\s*(1|0(?:\.\d+)?)\s*))?\)$/
25
+
26
+ const hexLookup = {}
27
+
28
+ const decimalToHex = (decimal) => {
29
+ if (hexLookup[decimal] !== undefined) {
30
+ return hexLookup[decimal]
31
+ }
32
+
33
+ const resp = parseInt(decimal).toString(16).padStart(2, '0')
34
+ hexLookup[decimal] = resp
35
+ return resp
36
+ }
20
37
 
21
38
  export default {
22
- // The defaultColor parameter should be set to null when utilizing this function within the template parser.
23
- // Because the template parser evaluates attribute values, even if they are not color codes,
24
- // to determine whether they represent color values or not.
25
- normalize(color = '', defaultColor = '0xffffffff') {
39
+ normalize: (color = '', defaultColor = '0xffffffff') => {
26
40
  color = color.toString()
27
41
 
28
- if (color.length > 0) {
29
- const normalized = /^0x[0-9a-f]{8}$/
30
- const hex = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i
31
- const rgba =
32
- /^(rgba?)\((\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*),(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*),(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\s*)(,(\s*(-?\d+(?:\.\d+)?)\s*))?\)$/
33
- const hsla =
34
- /^(hsla?)\((\s*(360|3[0-5][0-9]|[12]?[0-9]{1,2})\s*),(\s*(100|[1-9]?[0-9])\s*)%,(\s*(100|[1-9]?[0-9])\s*)%(,(\s*(1|0(?:\.\d+)?)\s*))?\)$/
42
+ // its mappable, map it
43
+ if (colorsMap[color] !== undefined) {
44
+ return colorsMap[color]
45
+ }
35
46
 
36
- if (normalized.test(color)) {
37
- return color
47
+ // its a 0xRRGGBBAA color, store it and return it
48
+ if (color.startsWith('0x') && color.length === 10) {
49
+ colorsMap[color] = color
50
+ return color
51
+ }
52
+
53
+ // check for hex color
54
+ if (hex.test(color)) {
55
+ color = color.replace('#', '').toLowerCase()
56
+ if (color.length === 3) {
57
+ color = color
58
+ .split('')
59
+ .map((c) => c + c)
60
+ .join('')
38
61
  }
39
62
 
40
- if (hex.test(color)) {
41
- color = color.replace('#', '').toLowerCase()
42
- if (color.length === 3) {
43
- color = color
44
- .split('')
45
- .map((c) => c + c)
46
- .join('')
47
- }
63
+ const colorRGBA = '0x' + color.padEnd(8, 'f')
64
+ colorsMap[color] = colorRGBA
65
+ return colorRGBA
66
+ }
48
67
 
49
- return '0x' + color.padEnd(8, 'f')
50
- }
68
+ //rgb/a
69
+ const rgbaMatch = rgba.exec(color)
70
+ if (rgbaMatch) {
71
+ const r = decimalToHex(rgbaMatch[3])
72
+ const g = decimalToHex(rgbaMatch[5])
73
+ const b = decimalToHex(rgbaMatch[7])
74
+ let a = 'ff'
51
75
 
52
- //rgb/a
53
- const rgbaMatch = rgba.exec(color)
54
- if (rgbaMatch) {
55
- return (
56
- '0x' +
57
- parseInt(rgbaMatch[3]).toString(16).padStart(2, '0') +
58
- parseInt(rgbaMatch[5]).toString(16).padStart(2, '0') +
59
- parseInt(rgbaMatch[7]).toString(16).padStart(2, '0') +
60
- (rgbaMatch[10] && rgbaMatch[1] === 'rgba'
61
- ? Math.min(Math.max(Math.round(parseFloat(rgbaMatch[10]) * 255), 0), 255)
62
- .toString(16)
63
- .padStart(2, '0')
64
- : 'ff')
65
- )
76
+ if (rgbaMatch[10] && rgbaMatch[1] === 'rgba') {
77
+ const alpha = Math.min(Math.max(Math.round(parseFloat(rgbaMatch[10]) * 255), 0), 255)
78
+ a = decimalToHex(alpha)
66
79
  }
67
80
 
68
- if (hsla.test(color)) {
69
- Log.warn('HSL(A) color format is not supported yet')
70
- return '0xffffffff'
71
- }
81
+ const resp = '0x' + r + g + b + a
82
+ colorsMap[color] = resp
83
+ return resp
84
+ }
72
85
 
73
- if (color in htmlColors) {
74
- return htmlColors[color]
75
- }
86
+ if (hsla.test(color)) {
87
+ console.warn('HSL(A) color format is not supported yet')
88
+ return '0xffffffff'
76
89
  }
77
90
 
78
91
  return defaultColor
@@ -23,7 +23,8 @@ export default (source, filePath) => {
23
23
  if (
24
24
  source.indexOf('Blits.Component(') > -1 ||
25
25
  source.indexOf('Blits.Application(') > -1 ||
26
- /=>\s*Component\(['"][A-Za-z]+['"],/s.test(source) // blits component
26
+ /=>\s*Component\(['"][A-Za-z]+['"],/s.test(source) || // blits component
27
+ /\{.*?template\s*:\s*(['"`])((?:\\?.)*?)\1.*?\}/s.test(source) // object with template key
27
28
  ) {
28
29
  const templates = source.matchAll(/(?<!\/\/\s*)template\s*:\s*(['"`])((?:\\?.)*?)\1/gs)
29
30
  let newSource = source
@@ -16,6 +16,7 @@
16
16
  */
17
17
 
18
18
  let currentEffect = null
19
+ let currentKey = null
19
20
 
20
21
  let paused = false
21
22
 
@@ -25,22 +26,16 @@ export const pauseTracking = () => {
25
26
 
26
27
  export const resumeTracking = () => {
27
28
  paused = false
28
- for (let i = 0; i < trackQueue.length; i++) {
29
- const item = trackQueue[i]
30
- currentEffect = item.currentEffect
31
- track(item.target, item.key)
32
- }
33
- trackQueue.length = 0
34
29
  }
35
30
 
36
31
  const objectMap = new WeakMap()
37
32
 
38
- const trackQueue = []
39
-
40
33
  export const track = (target, key) => {
41
34
  if (currentEffect) {
42
35
  if (paused) {
43
- trackQueue.push({ target, key, currentEffect })
36
+ return
37
+ }
38
+ if (currentKey !== null && key !== currentKey) {
44
39
  return
45
40
  }
46
41
  let effectsMap = objectMap.get(target)
@@ -58,21 +53,23 @@ export const track = (target, key) => {
58
53
  }
59
54
 
60
55
  export const trigger = (target, key, force = false) => {
61
- if (paused) return
56
+ if (paused === true) return
62
57
  const effectsMap = objectMap.get(target)
63
58
  if (!effectsMap) {
64
59
  return
65
60
  }
66
61
  const effects = effectsMap.get(key)
67
62
  if (effects) {
68
- effects.forEach((effect) => {
63
+ for (let effect of effects) {
69
64
  effect(force)
70
- })
65
+ }
71
66
  }
72
67
  }
73
68
 
74
- export const effect = (effect) => {
69
+ export const effect = (effect, key = null) => {
75
70
  currentEffect = effect
71
+ currentKey = key
76
72
  currentEffect()
77
73
  currentEffect = null
74
+ currentKey = null
78
75
  }
@@ -18,27 +18,27 @@
18
18
  import { track, trigger, pauseTracking, resumeTracking } from './effect.js'
19
19
  import symbols from '../symbols.js'
20
20
 
21
- const arrayPatchMethods = ['push', 'pop', 'shift', 'unshift', 'splice']
21
+ const arrayPatchMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort']
22
22
 
23
23
  const proxyMap = new WeakMap()
24
24
 
25
- const getProxy = (obj) => {
26
- return proxyMap.get(obj)
27
- }
28
-
29
- const getRaw = (value) => {
25
+ export const getRaw = (value) => {
30
26
  const raw = value && value[symbols.raw]
31
27
  return raw ? getRaw(raw) : value
32
28
  }
33
29
 
34
30
  const reactiveProxy = (original, _parent = null, _key) => {
35
- // don't create a proxy when a Blits component is assigned to a state variable
36
- if (typeof original === 'object' && original[symbols.id]) {
31
+ // don't create a proxy when a Blits component or an Image Texture
32
+ // is assigned to a state variable
33
+ if (
34
+ (typeof original === 'object' && original[symbols.id]) ||
35
+ original.constructor.name === '_ImageTexture'
36
+ ) {
37
37
  return original
38
38
  }
39
39
 
40
40
  // if original object is already a proxy, don't create a new one but return the existing one instead
41
- const existingProxy = getProxy(original)
41
+ const existingProxy = proxyMap.get(original)
42
42
  if (existingProxy) {
43
43
  return existingProxy
44
44
  }
@@ -52,21 +52,31 @@ const reactiveProxy = (original, _parent = null, _key) => {
52
52
 
53
53
  // handling arrays
54
54
  if (Array.isArray(target)) {
55
+ if (typeof target[key] === 'object' && target[key] !== null) {
56
+ if (Array.isArray(target[key])) {
57
+ track(target, key)
58
+ }
59
+ // create a new reactive proxy
60
+ return reactiveProxy(getRaw(target[key]), target, key)
61
+ }
55
62
  // augment array path methods (that change the length of the array)
56
- if (arrayPatchMethods.indexOf(key) > -1) {
63
+ if (arrayPatchMethods.indexOf(key) !== -1) {
57
64
  return function (...args) {
58
65
  pauseTracking()
59
66
  const result = target[key].apply(this, args)
67
+ resumeTracking()
60
68
  // trigger a change on the parent object and the key
61
69
  // i.e. when pushing a new item to `obj.data`, _parent will equal `obj`
62
70
  // and _key will equal `data`
63
- resumeTracking()
64
71
  trigger(_parent, _key)
65
72
  return result
66
73
  }
67
- } else {
68
- return Reflect.get(target, key, receiver)
69
74
  }
75
+ if (key === 'length') {
76
+ return original.length
77
+ }
78
+
79
+ return Reflect.get(target, key, receiver)
70
80
  }
71
81
 
72
82
  // handling objects (but not null values, which have object type in JS)
@@ -75,7 +85,7 @@ const reactiveProxy = (original, _parent = null, _key) => {
75
85
  track(target, key)
76
86
  }
77
87
  // create a new reactive proxy
78
- return reactiveProxy(target[key], target, key)
88
+ return reactiveProxy(getRaw(target[key]), target, key)
79
89
  }
80
90
 
81
91
  // handling all other types
@@ -3,6 +3,7 @@ export default {
3
3
  cursorTagStart: Symbol('cursorTagStart'),
4
4
  computedKeys: Symbol('computedKeys'),
5
5
  destroy: Symbol('destroy'),
6
+ getChildren: Symbol('getChildren'),
6
7
  holder: Symbol('holder'),
7
8
  id: Symbol('id'),
8
9
  identifier: Symbol('identifier'),
@@ -259,9 +259,23 @@ const removeView = async (route, view, transition) => {
259
259
  }
260
260
 
261
261
  const setOrAnimate = (node, transition, shouldAnimate = true) => {
262
- return shouldAnimate
263
- ? node.animate(transition.prop, transition.value, transition)
264
- : node.set(transition.prop, transition.value)
262
+ return new Promise((resolve) => {
263
+ if (shouldAnimate) {
264
+ // resolve the promise in the transition end-callback
265
+ // ("extending" end callback when one is already specified)
266
+ const existingEndCallback = transition.end
267
+ transition.end = existingEndCallback
268
+ ? (...args) => {
269
+ existingEndCallback(...args)
270
+ resolve()
271
+ }
272
+ : resolve
273
+ node.set(transition.prop, { transition })
274
+ } else {
275
+ node.set(transition.prop, transition.value)
276
+ resolve()
277
+ }
278
+ })
265
279
  }
266
280
 
267
281
  export const to = (location, data = {}, options = {}) => {
@@ -1,7 +1,25 @@
1
1
  import path from 'path'
2
2
  import * as fs from 'fs'
3
- import generateBMFont from 'msdf-bmfont-xml'
3
+ import { genFont, setGeneratePaths } from '@lightningjs/msdf-generator'
4
+ import { adjustFont } from '@lightningjs/msdf-generator/adjustFont'
4
5
 
6
+ class TaskQueue {
7
+ constructor() {
8
+ this.currentTask = Promise.resolve()
9
+ }
10
+
11
+ async enqueue(task) {
12
+ // Wait for the current task to complete before starting a new one
13
+ this.currentTask = this.currentTask.then(task, this.handleError)
14
+ await this.currentTask
15
+ }
16
+
17
+ handleError(error) {
18
+ console.error('Error during task execution:', error)
19
+ }
20
+ }
21
+
22
+ const fontGenerationQueue = new TaskQueue()
5
23
  let config
6
24
 
7
25
  export default function () {
@@ -15,14 +33,13 @@ export default function () {
15
33
  config = resolvedConfig
16
34
  buildOutputPath = config.build.outDir
17
35
  publicDir = path.join(config.root, 'public')
18
- msdfOutputDir = path.resolve(config.root, 'node_modules', '.tmp-msdf-fonts')
36
+ msdfOutputDir = path.resolve(config.root, 'node_modules', '.tmp-msdf-fonts-v2')
19
37
  },
20
38
  async configureServer(server) {
21
39
  server.middlewareMode = true
22
40
  server.middlewares.use('/', async (req, res, next) => {
23
41
  const file = req.url.substring(req.url.lastIndexOf('/') + 1)
24
42
  const match = file.match(/(.+)\.msdf\.(json|png)/)
25
-
26
43
  // msdf font request
27
44
  if (match) {
28
45
  const fontPath = path.dirname(req.url.split('?')[0]).replace(/^\//, '')
@@ -42,21 +59,28 @@ export default function () {
42
59
  )
43
60
  const mimeType = ext === 'png' ? 'image/png' : 'application/json'
44
61
 
45
- if (!fs.existsSync(generatedFontFile)) {
46
- console.log(`\nGenerating ${targetDir}/${fontName}.msdf.${ext}`)
47
- await generateFont(fontFile, targetDir, fontName)
48
- }
49
-
50
- const fileContent = fs.readFileSync(generatedFontFile)
51
-
52
- // Check if headers have already been sent
53
- if (!res.headersSent) {
54
- res.setHeader('Content-Type', mimeType)
55
- res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
56
- res.end(fileContent)
62
+ await fontGenerationQueue.enqueue(async () => {
63
+ if (!fs.existsSync(generatedFontFile)) {
64
+ // Check if generation is needed
65
+ console.log(`\nGenerating ${targetDir}/${fontName}.msdf.${ext}`)
66
+ await generateSDF(fontFile, path.join(targetDir))
67
+ }
68
+ })
69
+
70
+ if (fs.existsSync(generatedFontFile)) {
71
+ const fileContent = fs.readFileSync(generatedFontFile)
72
+
73
+ // Check if headers have already been sent
74
+ if (!res.headersSent) {
75
+ res.setHeader('Content-Type', mimeType)
76
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
77
+ res.end(fileContent)
78
+ } else {
79
+ // this should never happen except some edge cases
80
+ console.error(`ERROR: Headers already sent for ${req.url}`, res.getHeaders())
81
+ }
57
82
  } else {
58
- // this should never happen except some edge cases
59
- console.error(`ERROR: Headers already sent for ${req.url}`, res.getHeaders())
83
+ next() // Handle case where generation might have failed
60
84
  }
61
85
  } else {
62
86
  next() // ttf file does not exist
@@ -71,7 +95,6 @@ export default function () {
71
95
  },
72
96
  // after build ends, the first hook is renderStart where we can modify the output
73
97
  async renderStart() {
74
- // Find all TTF files under publicDir
75
98
  const ttfFiles = findAllTtfFiles(publicDir)
76
99
 
77
100
  for (const ttfFile of ttfFiles) {
@@ -83,7 +106,7 @@ export default function () {
83
106
  // Check if MSDF files are generated, if not, generate them
84
107
  if (!fs.existsSync(msdfJsonPath) || !fs.existsSync(msdfPngPath)) {
85
108
  console.log(`Generating missing MSDF files for ${ttfFile}`)
86
- await generateFont(ttfFile, path.join(msdfOutputDir, relativePath), baseName)
109
+ await generateSDF(ttfFile, path.join(msdfOutputDir, relativePath))
87
110
  }
88
111
  }
89
112
 
@@ -94,40 +117,16 @@ export default function () {
94
117
  }
95
118
  }
96
119
 
97
- const generateFont = (fontFile, fontDir, fontName) => {
98
- return new Promise((resolve, reject) => {
99
- if (!fs.existsSync(fontDir)) {
100
- fs.mkdirSync(fontDir, { recursive: true })
101
- }
102
- generateBMFont(
103
- fontFile,
104
- {
105
- outputType: 'json',
106
- },
107
- (err, textures, font) => {
108
- if (err) {
109
- console.error(err)
110
- reject(err)
111
- } else {
112
- textures.forEach((texture) => {
113
- try {
114
- fs.writeFileSync(path.resolve(fontDir, `${fontName}.msdf.png`), texture.texture)
115
- } catch (e) {
116
- console.error(e)
117
- reject(e)
118
- }
119
- })
120
- try {
121
- fs.writeFileSync(path.resolve(fontDir, `${fontName}.msdf.json`), font.data)
122
- resolve()
123
- } catch (e) {
124
- console.error(err)
125
- reject(e)
126
- }
127
- }
128
- }
129
- )
130
- })
120
+ const generateSDF = async (inputFilePath, outputDirPath) => {
121
+ // Ensure the destination directory exists
122
+ fs.mkdirSync(outputDirPath, { recursive: true })
123
+
124
+ setGeneratePaths(path.dirname(inputFilePath), outputDirPath)
125
+
126
+ let font = await genFont(path.basename(inputFilePath), 'msdf')
127
+
128
+ if (font) await adjustFont(font)
129
+ else console.error('Failed to generate MSDF file')
131
130
  }
132
131
 
133
132
  // finds all TTF files in a directory