forked from scratchfoundation/scratch-vm
-
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #156 from TurboWarp/fonts
Custom fonts - VM part
- Loading branch information
Showing
8 changed files
with
983 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
const EventEmitter = require('events'); | ||
const AssetUtil = require('../util/tw-asset-util'); | ||
const StringUtil = require('../util/string-util'); | ||
const log = require('../util/log'); | ||
|
||
/** | ||
* @typedef InternalFont | ||
* @property {boolean} system True if the font is built in to the system | ||
* @property {string} family The font's name | ||
* @property {string} fallback Fallback font family list | ||
* @property {Asset} [asset] scratch-storage asset if system: false | ||
*/ | ||
|
||
class FontManager extends EventEmitter { | ||
/** | ||
* @param {Runtime} runtime | ||
*/ | ||
constructor (runtime) { | ||
super(); | ||
this.runtime = runtime; | ||
/** @type {Array<InternalFont>} */ | ||
this.fonts = []; | ||
} | ||
|
||
/** | ||
* @param {string} family An unknown font family | ||
* @returns {boolean} true if the family is valid | ||
*/ | ||
isValidFamily (family) { | ||
return /^[-\w ]+$/.test(family); | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @returns {boolean} | ||
*/ | ||
hasFont (family) { | ||
return !!this.fonts.find(i => i.family === family); | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @returns {boolean} | ||
*/ | ||
getSafeName (family) { | ||
family = family.replace(/[^-\w ]/g, ''); | ||
return StringUtil.unusedName(family, this.fonts.map(i => i.family)); | ||
} | ||
|
||
changed () { | ||
this.emit('change'); | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @param {string} fallback | ||
*/ | ||
addSystemFont (family, fallback) { | ||
if (!this.isValidFamily(family)) { | ||
throw new Error('Invalid family'); | ||
} | ||
this.fonts.push({ | ||
system: true, | ||
family, | ||
fallback | ||
}); | ||
this.changed(); | ||
} | ||
|
||
/** | ||
* @param {string} family | ||
* @param {string} fallback | ||
* @param {Asset} asset scratch-storage asset | ||
*/ | ||
addCustomFont (family, fallback, asset) { | ||
if (!this.isValidFamily(family)) { | ||
throw new Error('Invalid family'); | ||
} | ||
|
||
this.fonts.push({ | ||
system: false, | ||
family, | ||
fallback, | ||
asset | ||
}); | ||
|
||
this.updateRenderer(); | ||
this.changed(); | ||
} | ||
|
||
/** | ||
* @returns {Array<{system: boolean; name: string; family: string; data: Uint8Array | null; format: string | null}>} | ||
*/ | ||
getFonts () { | ||
return this.fonts.map(font => ({ | ||
system: font.system, | ||
name: font.family, | ||
family: `"${font.family}", ${font.fallback}`, | ||
data: font.asset ? font.asset.data : null, | ||
format: font.asset ? font.asset.dataFormat : null | ||
})); | ||
} | ||
|
||
/** | ||
* @param {number} index Corresponds to index from getFonts() | ||
*/ | ||
deleteFont (index) { | ||
const [removed] = this.fonts.splice(index, 1); | ||
if (!removed.system) { | ||
this.updateRenderer(); | ||
} | ||
this.changed(); | ||
} | ||
|
||
clear () { | ||
const hadNonSystemFont = this.fonts.some(i => !i.system); | ||
this.fonts = []; | ||
if (hadNonSystemFont) { | ||
this.updateRenderer(); | ||
} | ||
this.changed(); | ||
} | ||
|
||
updateRenderer () { | ||
if (!this.runtime.renderer || !this.runtime.renderer.setCustomFonts) { | ||
return; | ||
} | ||
|
||
const fontfaces = {}; | ||
for (const font of this.fonts) { | ||
if (!font.system) { | ||
const uri = font.asset.encodeDataURI(); | ||
const fontface = `@font-face { font-family: "${font.family}"; src: url("${uri}"); }`; | ||
const family = `"${font.family}", ${font.fallback}`; | ||
fontfaces[family] = fontface; | ||
} | ||
} | ||
this.runtime.renderer.setCustomFonts(fontfaces); | ||
} | ||
|
||
/** | ||
* Get data to save in project.json and sb3 files. | ||
*/ | ||
serializeJSON () { | ||
if (this.fonts.length === 0) { | ||
return null; | ||
} | ||
|
||
return this.fonts.map(font => { | ||
const serialized = { | ||
system: font.system, | ||
family: font.family, | ||
fallback: font.fallback | ||
}; | ||
|
||
if (!font.system) { | ||
const asset = font.asset; | ||
serialized.md5ext = `${asset.assetId}.${asset.dataFormat}`; | ||
} | ||
|
||
return serialized; | ||
}); | ||
} | ||
|
||
/** | ||
* @returns {Asset[]} list of scratch-storage assets | ||
*/ | ||
serializeAssets () { | ||
return this.fonts | ||
.filter(i => !i.system) | ||
.map(i => i.asset); | ||
} | ||
|
||
/** | ||
* @param {unknown} json | ||
* @param {JSZip} [zip] | ||
* @param {boolean} [keepExisting] | ||
* @returns {Promise<void>} | ||
*/ | ||
async deserialize (json, zip, keepExisting) { | ||
if (!keepExisting) { | ||
this.clear(); | ||
} | ||
|
||
if (!Array.isArray(json)) { | ||
return; | ||
} | ||
|
||
for (const font of json) { | ||
if (!font || typeof font !== 'object') { | ||
continue; | ||
} | ||
|
||
try { | ||
const system = font.system; | ||
const family = font.family; | ||
const fallback = font.fallback; | ||
if ( | ||
typeof system !== 'boolean' || | ||
typeof family !== 'string' || | ||
typeof fallback !== 'string' || | ||
this.hasFont(family) | ||
) { | ||
continue; | ||
} | ||
|
||
if (system) { | ||
this.addSystemFont(family, fallback); | ||
} else { | ||
const md5ext = font.md5ext; | ||
if (typeof md5ext !== 'string') { | ||
continue; | ||
} | ||
|
||
const asset = await AssetUtil.getByMd5ext( | ||
this.runtime, | ||
zip, | ||
this.runtime.storage.AssetType.Font, | ||
md5ext | ||
); | ||
this.addCustomFont(family, fallback, asset); | ||
} | ||
} catch (e) { | ||
log.error('could not add font', e); | ||
} | ||
} | ||
} | ||
} | ||
|
||
module.exports = FontManager; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const StringUtil = require('./string-util'); | ||
|
||
class AssetUtil { | ||
/** | ||
* @param {Runtime} runtime runtime with storage attached | ||
* @param {JSZip} zip optional JSZip to search for asset in | ||
* @param {Storage.assetType} assetType scratch-storage asset type | ||
* @param {string} md5ext full md5 with file extension | ||
* @returns {Promise<Storage.Asset>} scratch-storage asset object | ||
*/ | ||
static getByMd5ext (runtime, zip, assetType, md5ext) { | ||
const storage = runtime.storage; | ||
const idParts = StringUtil.splitFirst(md5ext, '.'); | ||
const md5 = idParts[0]; | ||
const ext = idParts[1].toLowerCase(); | ||
|
||
if (zip) { | ||
// Search the root of the zip | ||
let file = zip.file(md5ext); | ||
|
||
// Search subfolders of the zip | ||
// This matches behavior of deserialize-assets.js | ||
if (!file) { | ||
const fileMatch = new RegExp(`^([^/]*/)?${md5ext}$`); | ||
file = zip.file(fileMatch)[0]; | ||
} | ||
|
||
if (file) { | ||
return file.async('uint8array').then(data => runtime.storage.createAsset( | ||
assetType, | ||
ext, | ||
data, | ||
md5, | ||
false | ||
)); | ||
} | ||
} | ||
|
||
return storage.load(assetType, md5, ext); | ||
} | ||
} | ||
|
||
module.exports = AssetUtil; |
Oops, something went wrong.