├── src ├── index.js └── lib │ ├── dfs.js │ ├── pixi-source │ ├── createIndicesForQuads.js │ ├── BatchBuffer.js │ └── checkMaxIfStatmentsInShader.js │ ├── parser │ └── dsl.pegjs │ ├── utils.js │ ├── TextStyle.js │ ├── SDFCache.js │ ├── SDF.js │ ├── generateMultiTextureShader.js │ ├── RichText.js │ ├── lru-cache │ ├── yallist.js │ └── index.js │ └── RichTextRenderer.js ├── .travis.yml ├── .babelrc ├── static ├── index.html └── demo.js ├── .gitignore ├── .npmignore ├── LICENSE ├── package.json ├── rollup.config.js ├── README.md └── test └── dsl.test.js /src/index.js: -------------------------------------------------------------------------------- 1 | // patch 2 | import 'babel-polyfill'; 3 | import './lib/RichTextRenderer'; 4 | import RichText from './lib/RichText'; 5 | export default RichText; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | - /^greenkeeper/.*$/ 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | notifications: 11 | email: false 12 | node_js: 13 | - node 14 | script: 15 | - npm run test:prod && npm run build 16 | after_success: 17 | - npm run report-coverage 18 | - npm run deploy-docs 19 | - npm run semantic-release 20 | -------------------------------------------------------------------------------- /src/lib/dfs.js: -------------------------------------------------------------------------------- 1 | export default function* dfs(node, defaultDepth = 0) { 2 | const children = (node instanceof Array) ? node : node.children; 3 | let depth = defaultDepth; 4 | 5 | for (const child of children) { 6 | yield [child, depth]; 7 | 8 | if (child.children && child.children.length) { 9 | depth += 1 10 | yield* dfs(child.children, depth); 11 | depth -= 1; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | ["env", { 5 | "modules": false, 6 | "loose": true, 7 | "useBuiltIns": "usage" 8 | }] 9 | ], 10 | "plugins": [ 11 | [ 12 | "transform-object-rest-spread", 13 | { 14 | "useBuiltIns": true 15 | } 16 | ], 17 | "external-helpers" 18 | ], 19 | "env": { 20 | "production": { 21 | "presets": ["minify"] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 测试文字 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ---> Node 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ---> Node 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | .babelrc 32 | .travis.yml 33 | rollup.config.js 34 | test 35 | scripts -------------------------------------------------------------------------------- /src/lib/pixi-source/createIndicesForQuads.js: -------------------------------------------------------------------------------- 1 | // createIndicesForQuads 2 | 3 | /** 4 | * Generic Mask Stack data structure 5 | * 6 | * @memberof PIXI 7 | * @function createIndicesForQuads 8 | * @private 9 | * @param {number} size - Number of quads 10 | * @return {Uint16Array} indices 11 | */ 12 | export default function createIndicesForQuads(size) 13 | { 14 | // the total number of indices in our array, there are 6 points per quad. 15 | 16 | const totalIndices = size * 6; 17 | 18 | const indices = new Uint16Array(totalIndices); 19 | 20 | // fill the indices with the quads to draw 21 | for (let i = 0, j = 0; i < totalIndices; i += 6, j += 4) 22 | { 23 | indices[i + 0] = j + 0; 24 | indices[i + 1] = j + 1; 25 | indices[i + 2] = j + 2; 26 | indices[i + 3] = j + 0; 27 | indices[i + 4] = j + 2; 28 | indices[i + 5] = j + 3; 29 | } 30 | 31 | return indices; 32 | } -------------------------------------------------------------------------------- /src/lib/pixi-source/BatchBuffer.js: -------------------------------------------------------------------------------- 1 | // pixi.js/lib/core/sprites/webgl/BatchBuffer 2 | 3 | /** 4 | * @class 5 | * @memberof PIXI 6 | */ 7 | export default class Buffer 8 | { 9 | /** 10 | * @param {number} size - The size of the buffer in bytes. 11 | */ 12 | constructor(size) 13 | { 14 | this.vertices = new ArrayBuffer(size); 15 | 16 | /** 17 | * View on the vertices as a Float32Array for positions 18 | * 19 | * @member {Float32Array} 20 | */ 21 | this.float32View = new Float32Array(this.vertices); 22 | 23 | /** 24 | * View on the vertices as a Uint32Array for uvs 25 | * 26 | * @member {Float32Array} 27 | */ 28 | this.uint32View = new Uint32Array(this.vertices); 29 | } 30 | 31 | /** 32 | * Destroys the buffer. 33 | * 34 | */ 35 | destroy() 36 | { 37 | this.vertices = null; 38 | this.positions = null; 39 | this.uvs = null; 40 | this.colors = null; 41 | } 42 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Icemic Jia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/lib/parser/dsl.pegjs: -------------------------------------------------------------------------------- 1 | // BBCode like Grammar (replace '[]' with '<>') 2 | // ========================== 3 | // 4 | 5 | Expression 6 | = Node+ 7 | 8 | Node 9 | = content:TagNode / content:TextNode { 10 | return content 11 | } 12 | 13 | TagNode 14 | = head:TagHead children:Node* tail:TagTail &{ return head[0] === tail } { 15 | return { 16 | tagName: head[0], 17 | value: head[1], 18 | nodeType: 'tag', 19 | children, 20 | } 21 | } 22 | 23 | TagHead 24 | = '<' tagName:TagName '=' value:TagValue '>' { 25 | return [tagName.join(''), value]; 26 | } 27 | / '<' tagName:TagName '>' { 28 | return [tagName.join('')]; 29 | } 30 | TagTail 31 | = '' { return tagName.join(''); } 32 | 33 | TagName 34 | = [^=]+ 35 | 36 | TagValue 37 | = Number / Boolean / String 38 | 39 | TextNode 40 | = String { 41 | return { 42 | nodeType: 'text', 43 | content: text(), 44 | } 45 | } 46 | 47 | String 48 | = [^<>]+ { 49 | return text() 50 | } 51 | 52 | Number 53 | = (('0x' [0-9A-Fa-f]+) / ([0-9]+)) & '>'{ 54 | return parseInt(text()) 55 | } 56 | 57 | Boolean 58 | = ('true'i / 'false'i) & '>' { 59 | return text().toLowerCase() === 'true' 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pixi-richtext", 3 | "version": "1.1.2", 4 | "description": "A Rich Text solution for PixiJS using SDF.", 5 | "main": "dist/pixi.richtext.umd.js", 6 | "browser": "dist/pixi.richtext.umd.js", 7 | "module": "dist/pixi.richtext.es5.js", 8 | "scripts": { 9 | "build": "NODE_ENV=production BABEL_ENV=production rollup -c rollup.config.js", 10 | "start": "http-server -p 3001 & rollup -c rollup.config.js -w", 11 | "prepack": "npm run build", 12 | "dev": "webpack-dev-server --config webpack.config.example.js --hot --host 0.0.0.0 --port 8001 --devtool source-map" 13 | }, 14 | "keywords": [ 15 | "pixi", 16 | "pixi.js", 17 | "webgl", 18 | "richtext", 19 | "rich-text", 20 | "sdf" 21 | ], 22 | "author": "Icemic ", 23 | "license": "MIT", 24 | "dependencies": { 25 | "bit-twiddle": "^1.0.2", 26 | "color": "^2.0.0", 27 | "huozi": "^1.1.1", 28 | "pixi.js": "^4.5.3" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.26.0", 32 | "babel-loader": "^7.1.4", 33 | "babel-plugin-external-helpers": "^6.22.0", 34 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 35 | "babel-preset-env": "^1.6.1", 36 | "babel-preset-minify": "^0.3.0", 37 | "pegjs": "^0.10.0", 38 | "rollup": "^0.56.5", 39 | "rollup-plugin-babel": "^3.0.3", 40 | "rollup-plugin-commonjs": "^9.1.0", 41 | "rollup-plugin-license": "^0.6.0", 42 | "rollup-plugin-node-resolve": "^3.3.0", 43 | "rollup-plugin-pegjs": "^2.1.3", 44 | "rollup-plugin-replace": "^2.0.0", 45 | "rollup-plugin-sourcemaps": "^0.4.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/pixi-source/checkMaxIfStatmentsInShader.js: -------------------------------------------------------------------------------- 1 | // pixi.js/lib/core/renderers/webgl/utils/checkMaxIfStatmentsInShader 2 | 3 | const glCore = PIXI.glCore; 4 | 5 | const fragTemplate = [ 6 | 'precision mediump float;', 7 | 'void main(void){', 8 | 'float test = 0.1;', 9 | '%forloop%', 10 | 'gl_FragColor = vec4(0.0);', 11 | '}', 12 | ].join('\n'); 13 | 14 | export default function checkMaxIfStatmentsInShader(maxIfs, gl) 15 | { 16 | const createTempContext = !gl; 17 | 18 | // @if DEBUG 19 | if (maxIfs === 0) 20 | { 21 | throw new Error('Invalid value of `0` passed to `checkMaxIfStatementsInShader`'); 22 | } 23 | // @endif 24 | 25 | if (createTempContext) 26 | { 27 | const tinyCanvas = document.createElement('canvas'); 28 | 29 | tinyCanvas.width = 1; 30 | tinyCanvas.height = 1; 31 | 32 | gl = glCore.createContext(tinyCanvas); 33 | } 34 | 35 | const shader = gl.createShader(gl.FRAGMENT_SHADER); 36 | 37 | while (true) // eslint-disable-line no-constant-condition 38 | { 39 | const fragmentSrc = fragTemplate.replace(/%forloop%/gi, generateIfTestSrc(maxIfs)); 40 | 41 | gl.shaderSource(shader, fragmentSrc); 42 | gl.compileShader(shader); 43 | 44 | if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) 45 | { 46 | maxIfs = (maxIfs / 2) | 0; 47 | } 48 | else 49 | { 50 | // valid! 51 | break; 52 | } 53 | } 54 | 55 | if (createTempContext) 56 | { 57 | // get rid of context 58 | if (gl.getExtension('WEBGL_lose_context')) 59 | { 60 | gl.getExtension('WEBGL_lose_context').loseContext(); 61 | } 62 | } 63 | 64 | return maxIfs; 65 | } 66 | 67 | function generateIfTestSrc(maxIfs) 68 | { 69 | let src = ''; 70 | 71 | for (let i = 0; i < maxIfs; ++i) 72 | { 73 | if (i > 0) 74 | { 75 | src += '\nelse '; 76 | } 77 | 78 | if (i < maxIfs - 1) 79 | { 80 | src += `if(test == ${i}.0){}`; 81 | } 82 | } 83 | 84 | return src; 85 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import sourceMaps from 'rollup-plugin-sourcemaps' 4 | import babel from 'rollup-plugin-babel'; 5 | import pegjs from 'rollup-plugin-pegjs'; 6 | import replace from 'rollup-plugin-replace'; 7 | import license from 'rollup-plugin-license'; 8 | // import builtins from 'rollup-plugin-node-builtins'; 9 | import path from 'path'; 10 | 11 | const pkg = require('./package.json'); 12 | 13 | const BANNER = ` 14 | <%= pkg.name %> - v<%= pkg.version %> 15 | Compiled <%= moment().format() %> 16 | 17 | @author <%= pkg.author %> 18 | 19 | @license <%= pkg.license %> 20 | <%= pkg.name %> is licensed under the MIT License. 21 | http://www.opensource.org/licenses/mit-license 22 | `; 23 | 24 | export default { 25 | input: `src/index.js`, 26 | output: [ 27 | { file: pkg.browser, name: 'PIXI.RichText', format: 'umd', sourcemap: true }, 28 | { file: pkg.module, format: 'es', sourcemap: true }, 29 | ], 30 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 31 | external: ['pixi.js', 'pixi-gl-core'], 32 | globals: { 33 | 'pixi.js': 'PIXI', 34 | 'pixi-gl-core': 'PIXI.glCore' 35 | }, 36 | watch: { 37 | include: 'src/**', 38 | }, 39 | plugins: [ 40 | replace({ 41 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), 42 | 'VERSION': pkg.version 43 | }), 44 | // builtins(), 45 | // pegjs 46 | pegjs(), 47 | // Compile ES6 files 48 | babel({ 49 | exclude: 'node_modules/**' 50 | }), 51 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 52 | commonjs(), 53 | // Allow node_modules resolution, so you can use 'external' to control 54 | // which external modules to include in the bundle 55 | // https://github.com/rollup/rollup-plugin-node-resolve#usage 56 | resolve({ 57 | // browser: true 58 | }), 59 | 60 | license({ 61 | banner: BANNER, 62 | thirdParty: { 63 | output: path.join(__dirname, 'dist', 'dependencies.txt'), 64 | includePrivate: false 65 | } 66 | }), 67 | 68 | // Resolve source maps to the original source 69 | sourceMaps(), 70 | ], 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import huozi from 'huozi'; 2 | 3 | import { getSDFTexture, getUVVertex, autoUpdateTexture, getSDFConfig } from './SDFCache'; 4 | import parser from './parser/dsl.pegjs'; 5 | import dfs from './dfs'; 6 | 7 | const sdfConfig = getSDFConfig(); 8 | 9 | const NODE_ENV = process.env.NODE_ENV; 10 | 11 | export function parseRichText(text, sprite) { 12 | 13 | var now = performance.now(); 14 | const ast = parser.parse(text); 15 | NODE_ENV !== 'production' && console.log(`富文本解析时间:${(performance.now() - now) << 0} ms`) 16 | 17 | // let plainText = ''; 18 | 19 | // for (const [node] of dfs(ast)) { 20 | // (node.nodeType === 'text') && (plainText += node.content); 21 | // } 22 | 23 | // var now = performance.now(); 24 | // const uvVertexList = getTextData(plainText); 25 | // NODE_ENV !== 'production' && console.log(`SDF 纹理生成/获得时间:${(performance.now() - now) << 0} ms`) 26 | 27 | const vertexList = []; 28 | let currentStyle = sprite.style.clone(); 29 | const styleStack = []; 30 | let lastDepth = 0; 31 | 32 | // let index = 0; 33 | let x = 0; 34 | let y = 0; 35 | 36 | var now = performance.now(); 37 | for (const [node, depth] of dfs(ast)) { 38 | if (depth - lastDepth < 0) { 39 | for (let i = lastDepth - depth; i > 0; i--) { 40 | currentStyle = styleStack.pop(); 41 | } 42 | } 43 | 44 | lastDepth = depth; 45 | 46 | if (node.nodeType === 'text') { 47 | const content = node.content; 48 | const end = content.length; 49 | 50 | const currentState = currentStyle.state; 51 | for (let i = 0; i < end; i++) { 52 | vertexList.push({ 53 | ...currentState, 54 | ...getUVVertex(content[i], currentState.fontFamily), 55 | worldAlpha: sprite.worldAlpha, 56 | tintRGB: sprite._tintRGB, 57 | blendMode: sprite.blendMode 58 | }); 59 | } 60 | } else { 61 | styleStack.push(currentStyle); 62 | currentStyle = currentStyle.clone(); 63 | 64 | if (Object.keys(currentStyle).includes(node.tagName)) { 65 | currentStyle[node.tagName] = (node.value === undefined) ? true : node.value; 66 | } 67 | } 68 | } 69 | autoUpdateTexture(); 70 | NODE_ENV !== 'production' && console.log(`树形到一维数组+纹理生成时间:${(performance.now() - now) << 0} ms`) 71 | 72 | 73 | var now = performance.now(); 74 | const list = huozi(vertexList, { 75 | ...sprite.style.layout, 76 | fixLeftQuote: false 77 | }); 78 | NODE_ENV !== 'production' && console.log(`排版时间:${(performance.now() - now) << 0} ms`) 79 | 80 | return list; 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pixi-richtext 2 | 3 | Rich-text for PixiJS! 4 | 5 | ## Feature 6 | 7 | - Runtime signed distance field algorithm, with LRU cache for 1024 characters 8 | - Better rendering effect 9 | - No font file required, while supporting custom font via `@font-face` 10 | - Full support for CJK languages 11 | - Layout using [huozi](https://github.com/Icemic/huozi.js) 12 | - Rich-text support 13 | 14 | 15 | ## Usage 16 | 17 | ```sh 18 | npm install pixi-richtext --save 19 | ``` 20 | 21 | ```js 22 | import PIXI from 'pixi.js'; 23 | import RichText from 'pixi-richtext'; 24 | 25 | const app = new PIXI.Application({ 26 | view: document.getElementById('app'), 27 | width: 1280, 28 | height: 720 29 | }); 30 | 31 | const chars = '泽材莫笔亡鲜,如何你的计师朋友'; 32 | // see more: https://github.com/Icemic/huozi.js 33 | const layoutOptions = { 34 | // any valid CSS font-family value is supported 35 | // includes the fonts imported using @font-face 36 | fontFamily: 'sans-serif', 37 | // grid width for layout, 1em = gridSize 38 | gridSize: 26, 39 | // max width presented by character count 40 | // (max-width = (gridSize + xInterval) * column - xInterval) 41 | column: 25, 42 | // max line number 43 | row: Infinity, 44 | // interval between characters (CJK only) 45 | xInterval: 0, 46 | // interval between lines 47 | yInterval: 12, 48 | // (for western characters) 49 | letterSpacing: 0, 50 | // compress punctuation inline (CJK only) 51 | inlineCompression: true, 52 | forceGridAlignment: true, 53 | // enable it if your text do not include CJK characters 54 | westernCharacterFirst: false, 55 | forceSpaceBetweenCJKAndWestern: false 56 | } 57 | 58 | const defaultStyle = { 59 | fillEnable: true, 60 | fillColor: 'black', 61 | fillAlpha: 1, 62 | 63 | shadowEnable: false, 64 | shadowColor: 'black', 65 | shadowAlpha: 1, 66 | shadowAngle: Math.PI / 6, 67 | shadowDistance: 5, 68 | shadowThickness: 2, 69 | shadowBlur: 0.15, 70 | 71 | strokeEnable: false, 72 | strokeColor: 'black', 73 | strokeAlpha: 1, 74 | strokeThickness: 0, 75 | 76 | fontFamily: 'sans-serif', 77 | fontSize: 18, 78 | 79 | italic: false, 80 | bold: false, 81 | 82 | // unsupported now 83 | strike: false, 84 | underline: false, 85 | 86 | layout: layoutOptions 87 | }; 88 | 89 | 90 | 91 | const text = new RichText(chars, defaultStyle, layoutOptions); 92 | app.stage.addChild(text); 93 | text.x = 0; 94 | text.y = 100; 95 | ``` 96 | 97 | Just like what you see above, rich-text is expressed by UBB-like code. All the tags in `defaultStyle` are supported. Tags in text will overwrite the style temporarily until the they were closed. 98 | 99 | In addition, you can use `text.renderPosition` to make a typewriter effect. See more in `example/demo.js`. 100 | 101 | ## Todos 102 | 103 | - [ ] Support pre-generated SDF texture 104 | - [ ] Generating SDF texture in single channel, which will expend cache number to 4096 105 | - [ ] Support strike and underline 106 | - [ ] Custom SDF algorithm, eg. [msdf](https://github.com/Chlumsky/msdfgen) 107 | 108 | ## Related Works 109 | 110 | - TinySDF (https://github.com/mapbox/tiny-sdf) 111 | 112 | ## Contribution 113 | 114 | Any contribution is welcomed! 115 | 116 | ## License 117 | 118 | MIT LICENSE, SEE [LICENSE](./LICENSE). 119 | 120 | -------------------------------------------------------------------------------- /src/lib/TextStyle.js: -------------------------------------------------------------------------------- 1 | import { getSDFTexture, getTextData, getSDFConfig } from './SDFCache'; 2 | const sdfConfig = getSDFConfig(); 3 | 4 | import Color from 'color'; 5 | 6 | const defaultStyle = { 7 | fontFamily: 'sans-serif', 8 | 9 | fillEnable: true, 10 | fillColor: 'black', 11 | fillAlpha: 1, 12 | 13 | shadowEnable: false, 14 | shadowColor: 'black', 15 | shadowAlpha: 1, 16 | shadowAngle: Math.PI / 6, 17 | shadowDistance: 5, 18 | shadowThickness: 2, 19 | shadowBlur: 0.15, // how to compute? 20 | // fillGradientType: TEXT_GRADIENT.LINEAR_VERTICAL, 21 | // fillGradientStops: [], 22 | 23 | strokeEnable: false, 24 | strokeColor: 'black', 25 | strokeAlpha: 1, 26 | strokeThickness: 0, 27 | 28 | fontSize: 18, 29 | 30 | italic: false, 31 | bold: false, 32 | strike: false, 33 | underline: false, 34 | }; 35 | 36 | const defaultLayoutStyle = { 37 | fontFamily: 'sans-serif', 38 | gridSize: 18, 39 | column: 25, 40 | row: Infinity, 41 | xInterval: 0, 42 | yInterval: 6, 43 | letterSpacing: 0, 44 | inlineCompression: true, 45 | forceGridAlignment: true, 46 | westernCharacterFirst: false, 47 | forceSpaceBetweenCJKAndWestern: false 48 | } 49 | 50 | export default class TextStyle { 51 | constructor(style, layoutStyle) { 52 | Object.assign(this, defaultStyle, style); 53 | this.layout = Object.assign({}, defaultLayoutStyle, layoutStyle); 54 | } 55 | clone() { 56 | // clone style 57 | const clonedProperties = {}; 58 | for (const key in defaultStyle) { 59 | clonedProperties[key] = this[key]; 60 | } 61 | 62 | // clone layout style 63 | clonedProperties.layout = Object.assign({}, this.layout); 64 | 65 | return new TextStyle(clonedProperties); 66 | } 67 | reset() { 68 | Object.assign(this, defaultStyle); 69 | Object.assign(this.layout, defaultLayoutStyle); 70 | } 71 | 72 | get state() { 73 | return { 74 | fontFamily: (this.fontFamily instanceof Array) ? this.fontFamily.join(',') : this.fontFamily, 75 | fontSize: this.fontSize, 76 | 77 | fillEnable: +this.fillEnable, 78 | fillColor: this.fillRGBA, 79 | fill: this.fill, 80 | 81 | strokeEnable: +this.strokeEnable, 82 | strokeColor: this.strokeRGBA, 83 | stroke: this.stroke, 84 | 85 | shadowEnable: +this.shadowEnable, 86 | shadowColor: this.shadowRGBA, 87 | shadowOffset: this.shadowOffset, 88 | shadow: this.shadow, 89 | shadowBlur: this.shadowBlur, 90 | 91 | italic: this.italic, 92 | 93 | gamma: this.gamma, 94 | baseTexture: getSDFTexture() 95 | }; 96 | } 97 | 98 | get shadowOffset() { 99 | const ratio = sdfConfig.size / this.fontSize; 100 | 101 | const coefficient = this.shadowDistance * ratio / sdfConfig.textureSize; 102 | const offsetX = Math.cos(this.shadowAngle) * coefficient; 103 | const offsetY = Math.sin(this.shadowAngle) * coefficient; 104 | 105 | return [offsetX, offsetY]; 106 | } 107 | get fillRGBA() { 108 | const fillRGB = Color(this.fillColor).rgbNumber(); 109 | return (fillRGB >> 16) + (fillRGB & 0xff00) + ((fillRGB & 0xff) << 16) + (this.fillAlpha * 255 << 24) 110 | } 111 | get strokeRGBA() { 112 | const strokeRGB = Color(this.strokeColor).rgbNumber(); 113 | return (strokeRGB >> 16) + (strokeRGB & 0xff00) + ((strokeRGB & 0xff) << 16) + (this.strokeAlpha * 255 << 24) 114 | } 115 | get shadowRGBA() { 116 | const shadowRGB = Color(this.shadowColor).rgbNumber(); 117 | return (shadowRGB >> 16) + (shadowRGB & 0xff00) + ((shadowRGB & 0xff) << 16) + (this.shadowAlpha * 255 << 24) 118 | } 119 | get fill() { 120 | return 0.74 - (+(!this.strokeEnable)) * 0.01 - +this.bold * 0.04; 121 | } 122 | get stroke() { 123 | return Math.max(0, this.strokeThickness * -0.06 + 0.7); 124 | } 125 | get shadow() { 126 | return Math.max(0, this.shadowThickness * -0.06 + 0.7); 127 | } 128 | get gamma() { 129 | return 2 * 1.4142 / (this.fontSize * ((!this.strokeEnable) ? 1 : 1.8)); 130 | } 131 | } -------------------------------------------------------------------------------- /src/lib/SDFCache.js: -------------------------------------------------------------------------------- 1 | import LRU from './lru-cache'; 2 | import TinySDF from './SDF'; 3 | 4 | const TEXTURESIZE = 2048; 5 | 6 | const CANVAS = document.createElement('canvas'); 7 | CANVAS.width = CANVAS.height = TEXTURESIZE; 8 | const CONTEXT = CANVAS.getContext('2d'); 9 | 10 | // document.body.appendChild(CANVAS) 11 | // window.CANVAS = CANVAS; 12 | 13 | const SDFSIZE = 54; 14 | const SDFBUFFER = 5; 15 | const SDFSIZEWITHBUFFER = SDFSIZE + SDFBUFFER * 2; 16 | 17 | const SDF = new TinySDF(SDFSIZE, SDFBUFFER, SDFSIZE / 3); 18 | 19 | let SDFTEXTURE = PIXI.BaseTexture.fromCanvas(CANVAS); 20 | SDFTEXTURE.mipmap = false; 21 | 22 | const DISPOSEDCACHE = []; 23 | 24 | const LRUMAXITEMCOUNT = 1024; 25 | 26 | const UVVERTEXCACHE = LRU({ 27 | max: LRUMAXITEMCOUNT, 28 | dispose: (key, value) => { 29 | DISPOSEDCACHE.push([value.x, value.y]); 30 | // console.log(`SDF 缓存:'${key}' 被释放`); 31 | } 32 | }); 33 | 34 | let currentX = 0; 35 | let currentY = 0; 36 | 37 | let sdfTextureNeedUpdate = false; 38 | 39 | export function getTextData(text) { 40 | 41 | if (text.length > LRUMAXITEMCOUNT) { 42 | console.warn(`[RichText] The number of characters to be rendered in one time exceeds the number in the cache, and the text rendering may appear with an error. To resolve it, you should enlarge the cache or divide the text to two part at least.`); 43 | } 44 | 45 | const retData = []; 46 | 47 | for (const char of text) { 48 | const uvVertex = getUVVertex(char); 49 | 50 | retData.push(uvVertex); 51 | } 52 | 53 | return retData; 54 | } 55 | 56 | export function getUVVertex(char, fontFamily) { 57 | let uvVertex = UVVERTEXCACHE.get(char + fontFamily); 58 | 59 | if (!uvVertex) { 60 | 61 | const sdfPixelData = SDF.renderToImageData(char, fontFamily); 62 | 63 | const [realW, realH] = SDF.measureCharacter(char, fontFamily); 64 | const ratio = realW / realH; 65 | 66 | const disposedPosition = DISPOSEDCACHE.shift(); 67 | 68 | let dx, dy; 69 | if (disposedPosition) { 70 | [dx, dy] = disposedPosition; 71 | } else { 72 | dx = currentX; 73 | dy = currentY; 74 | 75 | currentX += SDFSIZEWITHBUFFER; 76 | if (currentX > TEXTURESIZE - SDFSIZEWITHBUFFER) { 77 | currentX = 0; 78 | currentY += SDFSIZEWITHBUFFER; 79 | } 80 | if (currentY > TEXTURESIZE - SDFSIZEWITHBUFFER) { 81 | console.error('cache texture size overflow!'); 82 | } 83 | } 84 | 85 | const x0 = dx / TEXTURESIZE; 86 | const y0 = dy / TEXTURESIZE; 87 | 88 | const x1 = (dx + SDFSIZEWITHBUFFER * ratio) / TEXTURESIZE; 89 | const y1 = y0; 90 | 91 | const x2 = x1; 92 | const y2 = (dy + SDFSIZEWITHBUFFER) / TEXTURESIZE; 93 | 94 | const x3 = x0; 95 | const y3 = y2; 96 | 97 | const uv0 = (((y0 * 65535) & 0xFFFF) << 16) | ((x0 * 65535) & 0xFFFF); 98 | const uv1 = (((y1 * 65535) & 0xFFFF) << 16) | ((x1 * 65535) & 0xFFFF); 99 | const uv2 = (((y2 * 65535) & 0xFFFF) << 16) | ((x2 * 65535) & 0xFFFF); 100 | const uv3 = (((y3 * 65535) & 0xFFFF) << 16) | ((x3 * 65535) & 0xFFFF); 101 | 102 | uvVertex = { 103 | character: char, 104 | uvs: new Uint32Array([uv0, uv1, uv2, uv3]), 105 | realWidth: realW, 106 | realHeight: realH 107 | }; 108 | // [uv0, uv1, uv2, uv3, realW, realH, dx, dy]; 109 | 110 | UVVERTEXCACHE.set(char + fontFamily, uvVertex); 111 | 112 | sdfTextureNeedUpdate = true; 113 | 114 | CONTEXT.putImageData(sdfPixelData, dx, dy); 115 | } 116 | 117 | return uvVertex; 118 | } 119 | 120 | // must exec this method after executing getUVVertex() to update basetexture 121 | export function autoUpdateTexture() { 122 | if (sdfTextureNeedUpdate) { 123 | SDFTEXTURE.update(); 124 | sdfTextureNeedUpdate = false; 125 | } 126 | } 127 | 128 | export function getSDFTexture() { 129 | return SDFTEXTURE; 130 | } 131 | 132 | export function getSDFConfig() { 133 | return { 134 | size: SDFSIZE, 135 | buffer: SDFBUFFER, 136 | sizeWithBuffer: SDFSIZEWITHBUFFER, 137 | textureSize: TEXTURESIZE 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /static/demo.js: -------------------------------------------------------------------------------- 1 | const RichText = PIXI.RichText; 2 | 3 | PIXI.settings.RENDER_OPTIONS.antialias = true; 4 | var app = new PIXI.Application({ 5 | view: document.getElementById('app'), 6 | width: 880, 7 | height: 1080 8 | }); 9 | 10 | app.renderer.backgroundColor = 0xffffff; 11 | 12 | // var chars = `我与父亲不相见已二年余了,我最不能忘记的是他的背影。那年冬天,祖母死了,父亲的差使也交卸了,正是祸不单行的日子,我从北京到徐州,打算跟着父亲奔丧回家。到徐州见着父亲,看见满院狼藉的东西,又想起祖母,不禁簌簌地流下眼泪。父亲说,“事已如此,不必难过,好在天无绝人之路!” 13 | //   回家变卖典质,父亲还了亏空;又借钱办了丧事。这些日子,家中光景很是惨淡,一半为了丧事,一半为了父亲赋闲。丧事完毕,父亲要到南京谋事,我也要回北京念书,我们便同行。 14 | //   到南京时,有朋友约去游逛,勾留了一日;第二日上午便须渡江到浦口,下午上车北去。父亲因为事忙,本已说定不送我,叫旅馆里一个熟识的茶房陪我同去。他再三嘱咐茶房,甚是仔细。但他终于不放心,怕茶房不妥帖;颇踌躇了一会。其实我那年已二十岁,北京已来往过两三次,是没有甚么要紧的了。他踌躇了一会,终于决定还是自己送我去。我两三回劝他不必去;他只说,“不要紧,他们去不好!” 15 | //   我们过了江,进了车站。我买票,他忙着照看行李。行李太多了,得向脚夫行些小费,才可过去。他便又忙着和他们讲价钱。我那时真是聪明过分,总觉他说话不大漂亮,非自己插嘴不可。但他终于讲定了价钱;就送我上车。他给我拣定了靠车门的一张椅子;我将他给我做的紫毛大衣铺好坐位。他嘱我路上小心,夜里警醒些,不要受凉。又嘱托茶房好好照应我。我心里暗笑他的迂;他们只认得钱,托他们直是白托!而且我这样大年纪的人,难道还不能料理自己么?唉,我现在想想,那时真是太聪明了! 16 | //   我说道,“爸爸,你走吧。”他望车外看了看,说,“我买几个橘子去。你就在此地,不要走动。”我看那边月台的栅栏外有几个卖东西的等着顾客。走到那边月台,须穿过铁道,须跳下去又爬上去。父亲是一个胖子,走过去自然要费事些。我本来要去的,他不肯,只好让他去。我看见他戴着黑布小帽,穿着黑布大马褂,深青布棉袍,蹒跚地走到铁道边,慢慢探身下去,尚不大难。可是他穿过铁道,要爬上那边月台,就不容易了。他用两手攀着上面,两脚再向上缩;他肥胖的身子向左微倾,显出努力的样子。这时我看见他的背影,我的泪很快地流下来了。我赶紧拭干了泪,怕他看见,也怕别人看见。我再向外看时,他已抱了朱红的橘子望回走了。过铁道时,他先将橘子散放在地上,自己慢慢爬下,再抱起橘子走。到这边时,我赶紧去搀他。他和我走到车上,将橘子一股脑儿放在我的皮大衣上。于是扑扑衣上的泥土,心里很轻松似的,过一会说,“我走了;到那边来信!”我望着他走出去。他走了几步,回过头看见我,说,“进去吧,里边没人。”等他的背影混入来来往往的人里,再找不着了,我便进来坐下,我的眼泪又来了。 17 | //   近几年来,父亲和我都是东奔西走,家中光景是一日不如一日。他少年出外谋生,独力支持,做了许多大事。那知老境却如此颓唐!他触目伤怀,自然情不能自已。情郁于中,自然要发之于外;家庭琐屑便往往触他之怒。他待我渐渐不同往日。但最近两年的不见,他终于忘却我的不好,只是惦记着我,惦记着我的儿子。我北来后,他写了一信给我,信中说道,“我身体平安,惟膀子疼痛利害,举箸提笔,诸多不便,大约大去之期不远矣。”我读到此处,在晶莹的泪光中,又看见那肥胖的,青布棉袍,黑布马褂的背影。唉!我不知何时再能与他相见!`; 18 | // const chars = '泽材莫笔亡鲜,如何你的计师朋友'; 19 | 20 | const chars = 21 | `汉语(Chinese): 22 | 我与父亲不相见已二年余了,我最不能忘记的是他的背影。那年冬天,祖母死了,父亲的差使也交卸了,正是祸不单行的日子,我从北京到徐州,打算跟着父亲奔丧回家。到徐州见着父亲,看见满院狼藉的东西,又想起祖母,不禁簌簌地流下眼泪。父亲说,“事已如此,不必难过,好在天无绝人之路!” 23 | 回家变卖典质,父亲还了亏空;又借钱办了丧事。这些日子,家中光景很是惨淡,一半为了丧事,一半为了父亲赋闲。 24 | 25 | 日语(Japanese): 26 | 富士山は静岡県と山梨県に跨る活火山。標高は3776mであり日本最高峰である。古文献では不二とも書く。富士箱根伊豆国立公園に指定されている。その優美な風貌は、国内のみならず海外でも日本の象徴として広く知られている。芙蓉峰・富嶽などとも呼ばれる。古来より歌枕として著名である。 27 | 28 | Western : 29 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 30 | 31 | 混合(Mixed): 32 | 如同大部分北海道地区的地名由来,“札幌”这一地名也是起源于北海道当地的原住民阿伊努族的语言阿伊努语。关于札幌的名称起源有二种说法,一说认为札幌(さっぽろ)起源于阿伊努语中的“sat-poro-pet/サッ・ポロ・ペッ”,意指“干渴的大河”;另一说则认为起源于阿伊努语中的“sar-poro-pet/サリ・ポロ・ペッ”,意思是完全与前者颠倒的“有大片湿地的河流”。 33 | 34 | Quenya :P 35 | Áva turma quesset lá, ehtë isqua maquetta úr sat, foa et nírë inqua. Mardo vëaner axo er. Us men sundo métima, cëa na aiwë ronyo, mi pahta capië latin san. Tec yúyo onóro anwavë ëa, foa tanga rangwë at. Armar turúva mettarë úr nár, uë nulda linga lin, wán úr cíla raxë moina. 36 | 37 | Rich Text: 38 | 泽材莫笔亡鲜,如何你的计师朋友 39 | ` 40 | 41 | var text = new RichText(chars); 42 | app.stage.addChild(text); 43 | text.style.fontSize = 24; 44 | text.style.strokeEnable = false; 45 | text.style.strokeThickness = 3; 46 | text.style.strokeColor = 'white'; 47 | text.style.shadowEnable = false; 48 | text.style.shadowAngle = 0.707; 49 | text.style.shadowDistance = 0; 50 | text.style.shadowThickness = 4; 51 | text.style.shadowBlur = 0.15; 52 | 53 | text.style.layout.gridSize = 24; 54 | text.style.layout.column = 35; 55 | 56 | text.y = 0; 57 | 58 | const id = setInterval(() => { 59 | // text.y += 10; 60 | text.renderPosition += 1; 61 | 62 | if (text.renderPosition >= (text._vertexList.length || text.text.length)) { 63 | clearInterval(id); 64 | } 65 | }, 1000 / 50); 66 | -------------------------------------------------------------------------------- /src/lib/SDF.js: -------------------------------------------------------------------------------- 1 | const INF = 1e20; 2 | 3 | // 2D Euclidean distance transform by Felzenszwalb & Huttenlocher https://cs.brown.edu/~pff/dt/ 4 | function edt(data, width, height, f, d, v, z) { 5 | for (var x = 0; x < width; x++) { 6 | for (var y = 0; y < height; y++) { 7 | f[y] = data[y * width + x]; 8 | } 9 | edt1d(f, d, v, z, height); 10 | for (y = 0; y < height; y++) { 11 | data[y * width + x] = d[y]; 12 | } 13 | } 14 | for (y = 0; y < height; y++) { 15 | for (x = 0; x < width; x++) { 16 | f[x] = data[y * width + x]; 17 | } 18 | edt1d(f, d, v, z, width); 19 | for (x = 0; x < width; x++) { 20 | data[y * width + x] = Math.sqrt(d[x]); 21 | } 22 | } 23 | } 24 | 25 | // 1D squared distance transform 26 | function edt1d(f, d, v, z, n) { 27 | v[0] = 0; 28 | z[0] = -INF; 29 | z[1] = +INF; 30 | 31 | for (var q = 1, k = 0; q < n; q++) { 32 | var s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); 33 | while (s <= z[k]) { 34 | k--; 35 | s = ((f[q] + q * q) - (f[v[k]] + v[k] * v[k])) / (2 * q - 2 * v[k]); 36 | } 37 | k++; 38 | v[k] = q; 39 | z[k] = s; 40 | z[k + 1] = +INF; 41 | } 42 | 43 | for (q = 0, k = 0; q < n; q++) { 44 | while (z[k + 1] < q) k++; 45 | d[q] = (q - v[k]) * (q - v[k]) + f[v[k]]; 46 | } 47 | } 48 | 49 | /*! 50 | * copied and modified from https://github.com/mapbox/tiny-sdf/blob/master/index.js 51 | * @license Licensed under ISC 52 | * 53 | * @export 54 | * @class TinySDF 55 | */ 56 | export default class TinySDF { 57 | constructor(fontSize, buffer, radius, cutoff, fontFamily) { 58 | this.fontSize = fontSize || 24; 59 | this.buffer = buffer === undefined ? 3 : buffer; 60 | this.cutoff = cutoff || 0.25; 61 | this.fontFamily = fontFamily || 'sans-serif'; 62 | this.radius = radius || 8; 63 | var size = this.size = this.fontSize + this.buffer * 2; 64 | 65 | this.canvas = document.createElement('canvas'); 66 | this.canvas.width = this.canvas.height = size; 67 | 68 | this.ctx = this.canvas.getContext('2d'); 69 | this.ctx.font = this.fontSize + 'px ' + this.fontFamily; 70 | this.ctx.textBaseline = 'ideographic'; 71 | this.ctx.fillStyle = 'black'; 72 | this.ctx.lineJoin = 'miter'; 73 | this.ctx.miterLimit = 10; 74 | 75 | // temporary arrays for the distance transform 76 | this.gridOuter = new Float64Array(size * size); 77 | this.gridInner = new Float64Array(size * size); 78 | this.f = new Float64Array(size); 79 | this.d = new Float64Array(size); 80 | this.z = new Float64Array(size + 1); 81 | this.v = new Int16Array(size); 82 | 83 | // hack around https://bugzilla.mozilla.org/show_bug.cgi?id=737852 84 | this.middle = Math.round((size / 2) * 2) + 5//(navigator.userAgent.indexOf('Gecko/') >= 0 ? 1.2 : 1)); 85 | } 86 | renderToImageData(char, fontFamily = 'sans-serif') { 87 | this.fontFamily = fontFamily; 88 | this.ctx.font = this.fontSize + 'px ' + this.fontFamily; 89 | 90 | this.ctx.clearRect(0, 0, this.size, this.size); 91 | this.ctx.fillText(char, this.buffer, this.middle); 92 | 93 | var imgData = this.ctx.getImageData(0, 0, this.size, this.size); 94 | var data = imgData.data; 95 | 96 | for (var i = 0; i < this.size * this.size; i++) { 97 | var a = data[i * 4 + 3] / 255; // alpha value 98 | this.gridOuter[i] = a === 1 ? 0 : a === 0 ? INF : Math.pow(Math.max(0, 0.5 - a), 2); 99 | this.gridInner[i] = a === 1 ? INF : a === 0 ? 0 : Math.pow(Math.max(0, a - 0.5), 2); 100 | } 101 | 102 | edt(this.gridOuter, this.size, this.size, this.f, this.d, this.v, this.z); 103 | edt(this.gridInner, this.size, this.size, this.f, this.d, this.v, this.z); 104 | 105 | for (i = 0; i < this.size * this.size; i++) { 106 | var d = this.gridOuter[i] - this.gridInner[i]; 107 | var c = Math.max(0, Math.min(255, Math.round(255 - 255 * (d / this.radius + this.cutoff)))); 108 | data[4 * i + 0] = c; 109 | data[4 * i + 1] = c; 110 | data[4 * i + 2] = c; 111 | data[4 * i + 3] = 255; 112 | } 113 | 114 | return imgData; 115 | } 116 | measureCharacter(char, fontFamily = 'sans-serif') { 117 | this.fontFamily = fontFamily; 118 | this.ctx.font = this.fontSize + 'px ' + this.fontFamily; 119 | 120 | const width = this.ctx.measureText(char).width + this.buffer * 2; 121 | const height = this.size; 122 | 123 | return [width, height]; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/lib/generateMultiTextureShader.js: -------------------------------------------------------------------------------- 1 | // import Shader from '../../Shader'; 2 | // const PIXI = require('pixi.js'); 3 | // import * as PIXI from 'pixi.js'; 4 | const Shader = PIXI.Shader; 5 | // import { readFileSync } from 'fs'; 6 | // import { join } from 'path'; 7 | 8 | const vertexSrc = ` 9 | precision highp float; 10 | attribute vec2 aVertexPosition; 11 | attribute vec2 aTextureCoord; 12 | attribute vec4 aColor; 13 | attribute float aTextureId; 14 | attribute float aShadow; 15 | attribute float aStroke; 16 | attribute float aFill; 17 | attribute float aGamma; 18 | attribute float aShadowBlur; 19 | attribute vec4 aShadowColor; 20 | attribute vec4 aStrokeColor; 21 | attribute vec4 aFillColor; 22 | attribute vec2 aShadowOffset; 23 | attribute float aShadowEnable; 24 | attribute float aStrokeEnable; 25 | attribute float aFillEnable; 26 | 27 | uniform mat3 projectionMatrix; 28 | 29 | varying vec2 vTextureCoord; 30 | varying vec4 vColor; 31 | varying float vTextureId; 32 | varying float vShadow; 33 | varying float vStroke; 34 | varying float vFill; 35 | varying float vGamma; 36 | varying float vShadowBlur; 37 | varying vec4 vShadowColor; 38 | varying vec4 vStrokeColor; 39 | varying vec4 vFillColor; 40 | varying vec2 vShadowOffset; 41 | varying float vShadowEnable; 42 | varying float vStrokeEnable; 43 | varying float vFillEnable; 44 | 45 | void main(void){ 46 | gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0); 47 | 48 | vTextureCoord = aTextureCoord; 49 | vTextureId = aTextureId; 50 | vColor = vec4(aColor.rgb * aColor.a, aColor.a); 51 | vShadow = aShadow; 52 | vStroke = aStroke; 53 | vFill = aFill; 54 | vGamma = aGamma; 55 | vShadowColor = aShadowColor; 56 | vShadowBlur = aShadowBlur; 57 | vStrokeColor = aStrokeColor; 58 | vFillColor = aFillColor; 59 | vShadowOffset = aShadowOffset; 60 | vShadowEnable = aShadowEnable; 61 | vStrokeEnable = aStrokeEnable; 62 | vFillEnable = aFillEnable; 63 | } 64 | `; 65 | 66 | const fragTemplate = [ 67 | 'varying vec2 vTextureCoord;', 68 | 'varying vec4 vColor;', 69 | 'varying float vTextureId;', 70 | 'uniform sampler2D uSamplers[%count%];', 71 | // 'uniform float uStroke;', 72 | // 'uniform float uFill;', 73 | // 'uniform float uGamma;', 74 | // 'uniform vec3 uFillColor;', 75 | // 'uniform vec3 uStrokeColor;', 76 | 'varying float vShadow;', 77 | 'varying float vShadowBlur;', 78 | 'varying float vStroke;', 79 | 'varying float vFill;', 80 | 'varying float vGamma;', 81 | 'varying vec4 vShadowColor;', 82 | 'varying vec4 vStrokeColor;', 83 | 'varying vec4 vFillColor;', 84 | 'varying vec2 vShadowOffset;', 85 | 'varying float vShadowEnable;', 86 | 'varying float vStrokeEnable;', 87 | 'varying float vFillEnable;', 88 | 89 | 'void main(void){', 90 | // 'vec4 color;', 91 | 'float alphaStroke;', 92 | 'float alphaFill;', 93 | 'vec4 colorStroke;', 94 | 'float alphaShadow;', 95 | 'vec4 colorShadow;', 96 | 'vec4 colorFill;', 97 | 'float textureId = floor(vTextureId+0.5);', 98 | '%forloop%', 99 | 'vec4 color = mix(colorShadow * vShadowEnable, colorStroke, alphaStroke) * vColor;', 100 | 'gl_FragColor = mix(color, colorFill, alphaFill) * vColor;', 101 | '}', 102 | ].join('\n'); 103 | 104 | export default function generateMultiTextureShader(gl, maxTextures) 105 | { 106 | // const vertexSrc = readFileSync(join(__dirname, './texture.vert'), 'utf8'); 107 | let fragmentSrc = fragTemplate; 108 | 109 | fragmentSrc = fragmentSrc.replace(/%count%/gi, maxTextures); 110 | fragmentSrc = fragmentSrc.replace(/%forloop%/gi, generateSampleSrc(maxTextures)); 111 | 112 | const shader = new Shader(gl, vertexSrc, fragmentSrc); 113 | 114 | const sampleValues = []; 115 | 116 | for (let i = 0; i < maxTextures; i++) 117 | { 118 | sampleValues[i] = i; 119 | } 120 | 121 | shader.bind(); 122 | shader.uniforms.uSamplers = sampleValues; 123 | 124 | return shader; 125 | } 126 | 127 | function generateSampleSrc(maxTextures) 128 | { 129 | let src = ''; 130 | 131 | src += '\n'; 132 | src += '\n'; 133 | 134 | for (let i = 0; i < maxTextures; i++) 135 | { 136 | if (i > 0) 137 | { 138 | src += '\nelse '; 139 | } 140 | 141 | if (i < maxTextures - 1) 142 | { 143 | src += `if(textureId == ${i}.0)`; 144 | } 145 | 146 | src += `\n{ 147 | float dist = texture2D(uSamplers[${i}], vTextureCoord).r; 148 | float distOffset = texture2D(uSamplers[${i}], vTextureCoord.xy - vShadowOffset).r; 149 | alphaShadow = smoothstep(vShadow - vGamma - vShadowBlur, vShadow + vGamma + vShadowBlur, distOffset); 150 | alphaStroke = smoothstep(vStroke - vGamma, vStroke + vGamma, dist * vStrokeEnable); 151 | alphaFill = smoothstep(vFill - vGamma, vFill + vGamma, dist); 152 | colorShadow = vec4(vShadowColor.rgb, alphaShadow) * vShadowColor.a; 153 | colorStroke = vec4(vStrokeColor.rgb, alphaStroke) * vStrokeColor.a; 154 | colorFill = vec4(vFillColor.rgb, alphaFill * vFillEnable) * vFillColor.a; 155 | }`; 156 | 157 | // src += '\n{'; 158 | // src += `\n\tcolor = texture2D(uSamplers[${i}], vTextureCoord);`; 159 | // src += '\n}'; 160 | } 161 | 162 | src += '\n'; 163 | src += '\n'; 164 | 165 | return src; 166 | } -------------------------------------------------------------------------------- /src/lib/RichText.js: -------------------------------------------------------------------------------- 1 | // const PIXI = require('pixi.js'); 2 | // import * as PIXI from 'pixi.js'; 3 | import { parseRichText } from './utils'; 4 | import TextStyle from './TextStyle'; 5 | import { getSDFConfig } from './SDFCache'; 6 | 7 | const sdfConfig = getSDFConfig(); 8 | 9 | export default class RichText extends PIXI.Sprite { 10 | constructor(text, style = {}) { 11 | super(); 12 | 13 | this._style = new TextStyle(style, style.layout || {}); 14 | 15 | this._text = text; 16 | 17 | this._vertexList = []; 18 | this.needUpdateCharacter = true; 19 | this.needUpdateTypo = true; 20 | this.vertexCount = 0; 21 | 22 | /** 23 | * Control rendering position of text 24 | * Set the value to `n` means render the text from 0 to n 25 | * Set `-1` to disable. 26 | */ 27 | this.renderPosition = -1; 28 | 29 | /** 30 | * Plugin that is responsible for rendering this element. 31 | * Allows to customize the rendering process without overriding '_renderWebGL' & '_renderCanvas' methods. 32 | * 33 | * @member {string} 34 | * @default 'sprite' 35 | */ 36 | this.pluginName = 'richtext'; 37 | 38 | } 39 | set text(value) { 40 | if (this._text !== value) { 41 | this._text = value; 42 | this.needUpdateTypo = true; 43 | this.needUpdateCharacter = true; 44 | } 45 | } 46 | get text() { 47 | return this._text; 48 | } 49 | get style() { 50 | return this._style; 51 | } 52 | set style(value) { 53 | this._style = new TextStyle(value, value.layout || this._style.layout || {}); 54 | this.needUpdateTypo = true; 55 | this.needUpdateCharacter = true; 56 | } 57 | get layout() { 58 | return this._style.layout; 59 | } 60 | set layout(value) { 61 | this._style = new TextStyle(this._style, value || {}); 62 | this.needUpdateTypo = true; 63 | this.needUpdateCharacter = true; 64 | } 65 | get width() { 66 | const { gridSize, xInterval, column } = this._style.layout; 67 | 68 | const { x, y, realWidth, fontSize } = this._vertexList[this.renderPosition === 0 ? 0 : this.renderPosition > 0 ? (this.renderPosition - 1) : this._vertexList.length - 1]; 69 | if (y > 0) { 70 | return (gridSize + xInterval) * column - xInterval; 71 | } else { 72 | const ratio = fontSize / sdfConfig.size; 73 | const currentWidth = x + realWidth * ratio; 74 | return currentWidth; 75 | } 76 | } 77 | get height() { 78 | const { gridSize, yInterval, row } = this._style.layout; 79 | // if (isFinite(row)) { 80 | // return (gridSize + yInterval) * row - yInterval; 81 | // } else { 82 | const { y } = this._vertexList[this.renderPosition === 0 ? 0 : this.renderPosition > 0 ? (this.renderPosition - 1) : (this._vertexList.length - 1)]; 83 | return y + gridSize; 84 | // } 85 | } 86 | get vertexList() { 87 | if (this.renderPosition < 0) { 88 | return this._vertexList; 89 | } 90 | return this._vertexList.slice(0, this.renderPosition); 91 | } 92 | get cursorPosition() { 93 | const { x, y, realWidth, realHeight, fontSize } = this._vertexList[this.renderPosition === 0 ? 0 : this.renderPosition > 0 ? (this.renderPosition - 1) : (this._vertexList.length - 1)]; 94 | const ratio = fontSize / sdfConfig.size; 95 | return { x: x + realWidth * ratio, y: y }; 96 | } 97 | 98 | /** 99 | * Gets the local bounds of the text object. 100 | * 101 | * @param {Rectangle} rect - The output rectangle. 102 | * @return {Rectangle} The bounds. 103 | */ 104 | getLocalBounds(rect) { 105 | this.updateText(); 106 | return super.getLocalBounds.call(this, rect); 107 | } 108 | /** 109 | * calculates the bounds of the Text as a rectangle. The bounds calculation takes the worldTransform into account. 110 | */ 111 | _calculateBounds() { 112 | this.updateText(); 113 | 114 | const vertexData = this.calculateVertices(0, 0, this.width, this.height); 115 | 116 | // if we have already done this on THIS frame. 117 | this._bounds.addQuad(vertexData); 118 | } 119 | 120 | _renderWebGL(renderer) { 121 | const gl = renderer.gl; 122 | 123 | gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE); 124 | gl.enable(gl.BLEND); 125 | 126 | if (this._transformID !== this.transform._worldID) { 127 | this.needUpdateCharacter = true; 128 | this._transformID = this.transform._worldID; 129 | } 130 | 131 | if (this.needUpdateTypo || this.needUpdateCharacter) { 132 | this.updateText(); 133 | this.needUpdateCharacter = false; 134 | } 135 | 136 | renderer.setObjectRenderer(renderer.plugins[this.pluginName]); 137 | renderer.plugins[this.pluginName].render(this); 138 | } 139 | updateText() { 140 | if (this.needUpdateTypo) { 141 | this._vertexList = parseRichText(this._text, this); 142 | this.needUpdateTypo = false; 143 | } 144 | 145 | for (const item of this._vertexList) { 146 | const { realWidth, realHeight, x, y, fontSize, italic } = item; 147 | const ratio = fontSize / sdfConfig.size; 148 | const vertexData = this.calculateVertices(x, y, realWidth * ratio, realHeight * ratio, italic); 149 | 150 | if (vertexData) { 151 | Object.assign(item, { vertexData }); 152 | } 153 | } 154 | } 155 | calculateVertices(x, y, width, height, italic = false) { 156 | 157 | const worldTransform = this.transform.worldTransform; 158 | const anchor = this._anchor; 159 | 160 | // const { x: dx, y: dy } = sprite; 161 | 162 | const skewFactor = italic ? Math.sin(-0.207) : 0; 163 | 164 | const wt = worldTransform; 165 | const a = wt.a; 166 | const b = wt.b; 167 | const c = wt.c = skewFactor; 168 | const d = wt.d; 169 | const tx = wt.tx - y * skewFactor; 170 | const ty = wt.ty; 171 | // const anchor = this._anchor; 172 | let w0 = 0; 173 | let w1 = 0; 174 | let h0 = 0; 175 | let h1 = 0; 176 | 177 | // w1 = -anchor._x * orig.width; 178 | // w0 = w1 + orig.width; 179 | // h1 = -anchor._y * orig.height; 180 | // h0 = h1 + orig.height; 181 | 182 | // const ratio = fontSize / sdfConfig.size; 183 | 184 | w1 = (x - anchor._x * width); 185 | w0 = w1 + width; 186 | h1 = (y - anchor._y * height); 187 | h0 = h1 + height; 188 | 189 | const vertexData = []; 190 | // xy 191 | vertexData[0] = (a * w1) + (c * h1) + tx; 192 | vertexData[1] = (d * h1) + (b * w1) + ty; 193 | // xy 194 | vertexData[2] = (a * w0) + (c * h1) + tx; 195 | vertexData[3] = (d * h1) + (b * w0) + ty; 196 | // xy 197 | vertexData[4] = (a * w0) + (c * h0) + tx; 198 | vertexData[5] = (d * h0) + (b * w0) + ty; 199 | // xy 200 | vertexData[6] = (a * w1) + (c * h0) + tx; 201 | vertexData[7] = (d * h0) + (b * w1) + ty; 202 | 203 | return vertexData; 204 | } 205 | } -------------------------------------------------------------------------------- /test/dsl.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const fs = require('fs'); 3 | 4 | const peg = require("pegjs"); 5 | const parse = peg.generate(fs.readFileSync('./lib/parser/dsl.pegjs', 'utf8')).parse; 6 | 7 | describe('DSL Parser', () => { 8 | it('parse plain text', () => { 9 | expect(parse('hello world!')).to 10 | .eql([ 11 | { 12 | "nodeType": "text", 13 | "content": "hello world!" 14 | } 15 | ]); 16 | }); 17 | it('parse plain text (non-ascii)', () => { 18 | expect(parse('hello 世界!')).to 19 | .eql([ 20 | { 21 | "nodeType": "text", 22 | "content": "hello 世界!" 23 | } 24 | ]); 25 | }); 26 | it('parse tag node', () => { 27 | expect(parse('test')).to 28 | .eql([ 29 | { 30 | "tagName": "b", 31 | "value": undefined, 32 | "nodeType": "tag", 33 | "children": [ 34 | { 35 | "nodeType": "text", 36 | "content": "test" 37 | } 38 | ] 39 | } 40 | ]); 41 | }); 42 | it('parse tag node with value', () => { 43 | expect(parse('test')).to 44 | .eql([ 45 | { 46 | "tagName": "b", 47 | "value": 'aaa', 48 | "nodeType": "tag", 49 | "children": [ 50 | { 51 | "nodeType": "text", 52 | "content": "test" 53 | } 54 | ] 55 | } 56 | ]); 57 | }); 58 | it('parse tag node with value (non-ascii)', () => { 59 | expect(parse('<橙子=Touko>好吃')).to 60 | .eql([ 61 | { 62 | "tagName": "橙子", 63 | "value": 'Touko', 64 | "nodeType": "tag", 65 | "children": [ 66 | { 67 | "nodeType": "text", 68 | "content": "好吃" 69 | } 70 | ] 71 | } 72 | ]); 73 | }); 74 | it('parse tag node (empty content)', () => { 75 | expect(parse('')).to 76 | .eql([ 77 | { 78 | "tagName": "b", 79 | "value": undefined, 80 | "nodeType": "tag", 81 | "children": [] 82 | } 83 | ]); 84 | }); 85 | it('parse number value', () => { 86 | expect(parse('test')).to 87 | .eql([ 88 | { 89 | "tagName": "b", 90 | "value": 123, 91 | "nodeType": "tag", 92 | "children": [ 93 | { 94 | "nodeType": "text", 95 | "content": "test" 96 | } 97 | ] 98 | } 99 | ]); 100 | expect(parse('test')).to 101 | .eql([ 102 | { 103 | "tagName": "b", 104 | "value": 255, 105 | "nodeType": "tag", 106 | "children": [ 107 | { 108 | "nodeType": "text", 109 | "content": "test" 110 | } 111 | ] 112 | } 113 | ]); 114 | }); 115 | it('parse boolean value', () => { 116 | expect(parse('test')).to 117 | .eql([ 118 | { 119 | "tagName": "b", 120 | "value": true, 121 | "nodeType": "tag", 122 | "children": [ 123 | { 124 | "nodeType": "text", 125 | "content": "test" 126 | } 127 | ] 128 | } 129 | ]); 130 | expect(parse('test')).to 131 | .eql([ 132 | { 133 | "tagName": "b", 134 | "value": false, 135 | "nodeType": "tag", 136 | "children": [ 137 | { 138 | "nodeType": "text", 139 | "content": "test" 140 | } 141 | ] 142 | } 143 | ]); 144 | }); 145 | it('parse string value', () => { 146 | expect(parse('test')).to 147 | .eql([ 148 | { 149 | "tagName": "b", 150 | "value": 'value', 151 | "nodeType": "tag", 152 | "children": [ 153 | { 154 | "nodeType": "text", 155 | "content": "test" 156 | } 157 | ] 158 | } 159 | ]); 160 | expect(parse('test')).to 161 | .eql([ 162 | { 163 | "tagName": "b", 164 | "value": '123a', 165 | "nodeType": "tag", 166 | "children": [ 167 | { 168 | "nodeType": "text", 169 | "content": "test" 170 | } 171 | ] 172 | } 173 | ]); 174 | expect(parse('test')).to 175 | .eql([ 176 | { 177 | "tagName": "b", 178 | "value": 'false0', 179 | "nodeType": "tag", 180 | "children": [ 181 | { 182 | "nodeType": "text", 183 | "content": "test" 184 | } 185 | ] 186 | } 187 | ]); 188 | }); 189 | it('parse mixed nodes', () => { 190 | expect(parse('headtestmiddletestfoot')).to 191 | .eql([ 192 | { 193 | "nodeType": "text", 194 | "content": "head" 195 | }, 196 | { 197 | "tagName": "b", 198 | "value": 123, 199 | "nodeType": "tag", 200 | "children": [ 201 | { 202 | "nodeType": "text", 203 | "content": "test" 204 | } 205 | ] 206 | }, 207 | { 208 | "tagName": "b", 209 | "value": undefined, 210 | "nodeType": "tag", 211 | "children": [] 212 | }, 213 | { 214 | "nodeType": "text", 215 | "content": "middle" 216 | }, 217 | { 218 | "tagName": "b", 219 | "value": undefined, 220 | "nodeType": "tag", 221 | "children": [ 222 | { 223 | "nodeType": "text", 224 | "content": "test" 225 | } 226 | ] 227 | }, 228 | { 229 | "nodeType": "text", 230 | "content": "foot" 231 | } 232 | ]); 233 | }); 234 | it('parse nested nodes', () => { 235 | expect(parse('headtestlarge wordsx')).to 236 | .eql([ 237 | { 238 | "nodeType": "text", 239 | "content": "head" 240 | }, 241 | { 242 | "tagName": "b", 243 | "value": undefined, 244 | "nodeType": "tag", 245 | "children": [ 246 | { 247 | "nodeType": "text", 248 | "content": "test" 249 | }, 250 | { 251 | "tagName": "size", 252 | "value": 24, 253 | "nodeType": "tag", 254 | "children": [ 255 | { 256 | "tagName": "i", 257 | "value": undefined, 258 | "nodeType": "tag", 259 | "children": [ 260 | { 261 | "nodeType": "text", 262 | "content": "large" 263 | } 264 | ] 265 | }, 266 | { 267 | "nodeType": "text", 268 | "content": " words" 269 | } 270 | ] 271 | }, 272 | { 273 | "nodeType": "text", 274 | "content": "x" 275 | } 276 | ] 277 | } 278 | ]); 279 | }); 280 | it('throw error when tag doesn\'t match', () => { 281 | expect(() => parse('test')).to.throw(/Expected \[\^<\/>=\] but ">" found\./) 282 | }); 283 | }) 284 | -------------------------------------------------------------------------------- /src/lib/lru-cache/yallist.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * The ISC License 3 | * 4 | * Copyright (c) Isaac Z. Schlueter and Contributors 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 16 | * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | 'use strict' 20 | module.exports = Yallist 21 | 22 | Yallist.Node = Node 23 | Yallist.create = Yallist 24 | 25 | function Yallist (list) { 26 | var self = this 27 | if (!(self instanceof Yallist)) { 28 | self = new Yallist() 29 | } 30 | 31 | self.tail = null 32 | self.head = null 33 | self.length = 0 34 | 35 | if (list && typeof list.forEach === 'function') { 36 | list.forEach(function (item) { 37 | self.push(item) 38 | }) 39 | } else if (arguments.length > 0) { 40 | for (var i = 0, l = arguments.length; i < l; i++) { 41 | self.push(arguments[i]) 42 | } 43 | } 44 | 45 | return self 46 | } 47 | 48 | Yallist.prototype.removeNode = function (node) { 49 | if (node.list !== this) { 50 | throw new Error('removing node which does not belong to this list') 51 | } 52 | 53 | var next = node.next 54 | var prev = node.prev 55 | 56 | if (next) { 57 | next.prev = prev 58 | } 59 | 60 | if (prev) { 61 | prev.next = next 62 | } 63 | 64 | if (node === this.head) { 65 | this.head = next 66 | } 67 | if (node === this.tail) { 68 | this.tail = prev 69 | } 70 | 71 | node.list.length-- 72 | node.next = null 73 | node.prev = null 74 | node.list = null 75 | } 76 | 77 | Yallist.prototype.unshiftNode = function (node) { 78 | if (node === this.head) { 79 | return 80 | } 81 | 82 | if (node.list) { 83 | node.list.removeNode(node) 84 | } 85 | 86 | var head = this.head 87 | node.list = this 88 | node.next = head 89 | if (head) { 90 | head.prev = node 91 | } 92 | 93 | this.head = node 94 | if (!this.tail) { 95 | this.tail = node 96 | } 97 | this.length++ 98 | } 99 | 100 | Yallist.prototype.pushNode = function (node) { 101 | if (node === this.tail) { 102 | return 103 | } 104 | 105 | if (node.list) { 106 | node.list.removeNode(node) 107 | } 108 | 109 | var tail = this.tail 110 | node.list = this 111 | node.prev = tail 112 | if (tail) { 113 | tail.next = node 114 | } 115 | 116 | this.tail = node 117 | if (!this.head) { 118 | this.head = node 119 | } 120 | this.length++ 121 | } 122 | 123 | Yallist.prototype.push = function () { 124 | for (var i = 0, l = arguments.length; i < l; i++) { 125 | push(this, arguments[i]) 126 | } 127 | return this.length 128 | } 129 | 130 | Yallist.prototype.unshift = function () { 131 | for (var i = 0, l = arguments.length; i < l; i++) { 132 | unshift(this, arguments[i]) 133 | } 134 | return this.length 135 | } 136 | 137 | Yallist.prototype.pop = function () { 138 | if (!this.tail) { 139 | return undefined 140 | } 141 | 142 | var res = this.tail.value 143 | this.tail = this.tail.prev 144 | if (this.tail) { 145 | this.tail.next = null 146 | } else { 147 | this.head = null 148 | } 149 | this.length-- 150 | return res 151 | } 152 | 153 | Yallist.prototype.shift = function () { 154 | if (!this.head) { 155 | return undefined 156 | } 157 | 158 | var res = this.head.value 159 | this.head = this.head.next 160 | if (this.head) { 161 | this.head.prev = null 162 | } else { 163 | this.tail = null 164 | } 165 | this.length-- 166 | return res 167 | } 168 | 169 | Yallist.prototype.forEach = function (fn, thisp) { 170 | thisp = thisp || this 171 | for (var walker = this.head, i = 0; walker !== null; i++) { 172 | fn.call(thisp, walker.value, i, this) 173 | walker = walker.next 174 | } 175 | } 176 | 177 | Yallist.prototype.forEachReverse = function (fn, thisp) { 178 | thisp = thisp || this 179 | for (var walker = this.tail, i = this.length - 1; walker !== null; i--) { 180 | fn.call(thisp, walker.value, i, this) 181 | walker = walker.prev 182 | } 183 | } 184 | 185 | Yallist.prototype.get = function (n) { 186 | for (var i = 0, walker = this.head; walker !== null && i < n; i++) { 187 | // abort out of the list early if we hit a cycle 188 | walker = walker.next 189 | } 190 | if (i === n && walker !== null) { 191 | return walker.value 192 | } 193 | } 194 | 195 | Yallist.prototype.getReverse = function (n) { 196 | for (var i = 0, walker = this.tail; walker !== null && i < n; i++) { 197 | // abort out of the list early if we hit a cycle 198 | walker = walker.prev 199 | } 200 | if (i === n && walker !== null) { 201 | return walker.value 202 | } 203 | } 204 | 205 | Yallist.prototype.map = function (fn, thisp) { 206 | thisp = thisp || this 207 | var res = new Yallist() 208 | for (var walker = this.head; walker !== null;) { 209 | res.push(fn.call(thisp, walker.value, this)) 210 | walker = walker.next 211 | } 212 | return res 213 | } 214 | 215 | Yallist.prototype.mapReverse = function (fn, thisp) { 216 | thisp = thisp || this 217 | var res = new Yallist() 218 | for (var walker = this.tail; walker !== null;) { 219 | res.push(fn.call(thisp, walker.value, this)) 220 | walker = walker.prev 221 | } 222 | return res 223 | } 224 | 225 | Yallist.prototype.reduce = function (fn, initial) { 226 | var acc 227 | var walker = this.head 228 | if (arguments.length > 1) { 229 | acc = initial 230 | } else if (this.head) { 231 | walker = this.head.next 232 | acc = this.head.value 233 | } else { 234 | throw new TypeError('Reduce of empty list with no initial value') 235 | } 236 | 237 | for (var i = 0; walker !== null; i++) { 238 | acc = fn(acc, walker.value, i) 239 | walker = walker.next 240 | } 241 | 242 | return acc 243 | } 244 | 245 | Yallist.prototype.reduceReverse = function (fn, initial) { 246 | var acc 247 | var walker = this.tail 248 | if (arguments.length > 1) { 249 | acc = initial 250 | } else if (this.tail) { 251 | walker = this.tail.prev 252 | acc = this.tail.value 253 | } else { 254 | throw new TypeError('Reduce of empty list with no initial value') 255 | } 256 | 257 | for (var i = this.length - 1; walker !== null; i--) { 258 | acc = fn(acc, walker.value, i) 259 | walker = walker.prev 260 | } 261 | 262 | return acc 263 | } 264 | 265 | Yallist.prototype.toArray = function () { 266 | var arr = new Array(this.length) 267 | for (var i = 0, walker = this.head; walker !== null; i++) { 268 | arr[i] = walker.value 269 | walker = walker.next 270 | } 271 | return arr 272 | } 273 | 274 | Yallist.prototype.toArrayReverse = function () { 275 | var arr = new Array(this.length) 276 | for (var i = 0, walker = this.tail; walker !== null; i++) { 277 | arr[i] = walker.value 278 | walker = walker.prev 279 | } 280 | return arr 281 | } 282 | 283 | Yallist.prototype.slice = function (from, to) { 284 | to = to || this.length 285 | if (to < 0) { 286 | to += this.length 287 | } 288 | from = from || 0 289 | if (from < 0) { 290 | from += this.length 291 | } 292 | var ret = new Yallist() 293 | if (to < from || to < 0) { 294 | return ret 295 | } 296 | if (from < 0) { 297 | from = 0 298 | } 299 | if (to > this.length) { 300 | to = this.length 301 | } 302 | for (var i = 0, walker = this.head; walker !== null && i < from; i++) { 303 | walker = walker.next 304 | } 305 | for (; walker !== null && i < to; i++, walker = walker.next) { 306 | ret.push(walker.value) 307 | } 308 | return ret 309 | } 310 | 311 | Yallist.prototype.sliceReverse = function (from, to) { 312 | to = to || this.length 313 | if (to < 0) { 314 | to += this.length 315 | } 316 | from = from || 0 317 | if (from < 0) { 318 | from += this.length 319 | } 320 | var ret = new Yallist() 321 | if (to < from || to < 0) { 322 | return ret 323 | } 324 | if (from < 0) { 325 | from = 0 326 | } 327 | if (to > this.length) { 328 | to = this.length 329 | } 330 | for (var i = this.length, walker = this.tail; walker !== null && i > to; i--) { 331 | walker = walker.prev 332 | } 333 | for (; walker !== null && i > from; i--, walker = walker.prev) { 334 | ret.push(walker.value) 335 | } 336 | return ret 337 | } 338 | 339 | Yallist.prototype.reverse = function () { 340 | var head = this.head 341 | var tail = this.tail 342 | for (var walker = head; walker !== null; walker = walker.prev) { 343 | var p = walker.prev 344 | walker.prev = walker.next 345 | walker.next = p 346 | } 347 | this.head = tail 348 | this.tail = head 349 | return this 350 | } 351 | 352 | function push (self, item) { 353 | self.tail = new Node(item, self.tail, null, self) 354 | if (!self.head) { 355 | self.head = self.tail 356 | } 357 | self.length++ 358 | } 359 | 360 | function unshift (self, item) { 361 | self.head = new Node(item, null, self.head, self) 362 | if (!self.tail) { 363 | self.tail = self.head 364 | } 365 | self.length++ 366 | } 367 | 368 | function Node (value, prev, next, list) { 369 | if (!(this instanceof Node)) { 370 | return new Node(value, prev, next, list) 371 | } 372 | 373 | this.list = list 374 | this.value = value 375 | 376 | if (prev) { 377 | prev.next = this 378 | this.prev = prev 379 | } else { 380 | this.prev = null 381 | } 382 | 383 | if (next) { 384 | next.prev = this 385 | this.next = next 386 | } else { 387 | this.next = null 388 | } 389 | } 390 | 391 | // try { 392 | // // add if support or Symbol.iterator is present 393 | // require('./iterator.js') 394 | // } catch (er) {} 395 | 396 | Yallist.prototype[Symbol.iterator] = function* () { 397 | for (let walker = this.head; walker; walker = walker.next) { 398 | yield walker.value 399 | } 400 | } -------------------------------------------------------------------------------- /src/lib/lru-cache/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * The ISC License 3 | * 4 | * Copyright (c) Isaac Z. Schlueter and Contributors 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 16 | * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | 'use strict' 20 | 21 | module.exports = LRUCache 22 | 23 | // This will be a proper iterable 'Map' in engines that support it, 24 | // or a fakey-fake PseudoMap in older versions. 25 | // var Map = require('pseudomap') 26 | // var util = require('util') 27 | 28 | // A linked list to keep track of recently-used-ness 29 | var Yallist = require('./yallist') 30 | 31 | // use symbols if possible, otherwise just _props 32 | var hasSymbol = typeof Symbol === 'function' 33 | var makeSymbol 34 | if (hasSymbol) { 35 | makeSymbol = function (key) { 36 | return Symbol(key) 37 | } 38 | } else { 39 | makeSymbol = function (key) { 40 | return '_' + key 41 | } 42 | } 43 | 44 | var MAX = makeSymbol('max') 45 | var LENGTH = makeSymbol('length') 46 | var LENGTH_CALCULATOR = makeSymbol('lengthCalculator') 47 | var ALLOW_STALE = makeSymbol('allowStale') 48 | var MAX_AGE = makeSymbol('maxAge') 49 | var DISPOSE = makeSymbol('dispose') 50 | var NO_DISPOSE_ON_SET = makeSymbol('noDisposeOnSet') 51 | var LRU_LIST = makeSymbol('lruList') 52 | var CACHE = makeSymbol('cache') 53 | 54 | function naiveLength () { return 1 } 55 | 56 | // lruList is a yallist where the head is the youngest 57 | // item, and the tail is the oldest. the list contains the Hit 58 | // objects as the entries. 59 | // Each Hit object has a reference to its Yallist.Node. This 60 | // never changes. 61 | // 62 | // cache is a Map (or PseudoMap) that matches the keys to 63 | // the Yallist.Node object. 64 | function LRUCache (options) { 65 | if (!(this instanceof LRUCache)) { 66 | return new LRUCache(options) 67 | } 68 | 69 | if (typeof options === 'number') { 70 | options = { max: options } 71 | } 72 | 73 | if (!options) { 74 | options = {} 75 | } 76 | 77 | var max = this[MAX] = options.max 78 | // Kind of weird to have a default max of Infinity, but oh well. 79 | if (!max || 80 | !(typeof max === 'number') || 81 | max <= 0) { 82 | this[MAX] = Infinity 83 | } 84 | 85 | var lc = options.length || naiveLength 86 | if (typeof lc !== 'function') { 87 | lc = naiveLength 88 | } 89 | this[LENGTH_CALCULATOR] = lc 90 | 91 | this[ALLOW_STALE] = options.stale || false 92 | this[MAX_AGE] = options.maxAge || 0 93 | this[DISPOSE] = options.dispose 94 | this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false 95 | this.reset() 96 | } 97 | 98 | // resize the cache when the max changes. 99 | Object.defineProperty(LRUCache.prototype, 'max', { 100 | set: function (mL) { 101 | if (!mL || !(typeof mL === 'number') || mL <= 0) { 102 | mL = Infinity 103 | } 104 | this[MAX] = mL 105 | trim(this) 106 | }, 107 | get: function () { 108 | return this[MAX] 109 | }, 110 | enumerable: true 111 | }) 112 | 113 | Object.defineProperty(LRUCache.prototype, 'allowStale', { 114 | set: function (allowStale) { 115 | this[ALLOW_STALE] = !!allowStale 116 | }, 117 | get: function () { 118 | return this[ALLOW_STALE] 119 | }, 120 | enumerable: true 121 | }) 122 | 123 | Object.defineProperty(LRUCache.prototype, 'maxAge', { 124 | set: function (mA) { 125 | if (!mA || !(typeof mA === 'number') || mA < 0) { 126 | mA = 0 127 | } 128 | this[MAX_AGE] = mA 129 | trim(this) 130 | }, 131 | get: function () { 132 | return this[MAX_AGE] 133 | }, 134 | enumerable: true 135 | }) 136 | 137 | // resize the cache when the lengthCalculator changes. 138 | Object.defineProperty(LRUCache.prototype, 'lengthCalculator', { 139 | set: function (lC) { 140 | if (typeof lC !== 'function') { 141 | lC = naiveLength 142 | } 143 | if (lC !== this[LENGTH_CALCULATOR]) { 144 | this[LENGTH_CALCULATOR] = lC 145 | this[LENGTH] = 0 146 | this[LRU_LIST].forEach(function (hit) { 147 | hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key) 148 | this[LENGTH] += hit.length 149 | }, this) 150 | } 151 | trim(this) 152 | }, 153 | get: function () { return this[LENGTH_CALCULATOR] }, 154 | enumerable: true 155 | }) 156 | 157 | Object.defineProperty(LRUCache.prototype, 'length', { 158 | get: function () { return this[LENGTH] }, 159 | enumerable: true 160 | }) 161 | 162 | Object.defineProperty(LRUCache.prototype, 'itemCount', { 163 | get: function () { return this[LRU_LIST].length }, 164 | enumerable: true 165 | }) 166 | 167 | LRUCache.prototype.rforEach = function (fn, thisp) { 168 | thisp = thisp || this 169 | for (var walker = this[LRU_LIST].tail; walker !== null;) { 170 | var prev = walker.prev 171 | forEachStep(this, fn, walker, thisp) 172 | walker = prev 173 | } 174 | } 175 | 176 | function forEachStep (self, fn, node, thisp) { 177 | var hit = node.value 178 | if (isStale(self, hit)) { 179 | del(self, node) 180 | if (!self[ALLOW_STALE]) { 181 | hit = undefined 182 | } 183 | } 184 | if (hit) { 185 | fn.call(thisp, hit.value, hit.key, self) 186 | } 187 | } 188 | 189 | LRUCache.prototype.forEach = function (fn, thisp) { 190 | thisp = thisp || this 191 | for (var walker = this[LRU_LIST].head; walker !== null;) { 192 | var next = walker.next 193 | forEachStep(this, fn, walker, thisp) 194 | walker = next 195 | } 196 | } 197 | 198 | LRUCache.prototype.keys = function () { 199 | return this[LRU_LIST].toArray().map(function (k) { 200 | return k.key 201 | }, this) 202 | } 203 | 204 | LRUCache.prototype.values = function () { 205 | return this[LRU_LIST].toArray().map(function (k) { 206 | return k.value 207 | }, this) 208 | } 209 | 210 | LRUCache.prototype.reset = function () { 211 | if (this[DISPOSE] && 212 | this[LRU_LIST] && 213 | this[LRU_LIST].length) { 214 | this[LRU_LIST].forEach(function (hit) { 215 | this[DISPOSE](hit.key, hit.value) 216 | }, this) 217 | } 218 | 219 | this[CACHE] = new Map() // hash of items by key 220 | this[LRU_LIST] = new Yallist() // list of items in order of use recency 221 | this[LENGTH] = 0 // length of items in the list 222 | } 223 | 224 | LRUCache.prototype.dump = function () { 225 | return this[LRU_LIST].map(function (hit) { 226 | if (!isStale(this, hit)) { 227 | return { 228 | k: hit.key, 229 | v: hit.value, 230 | e: hit.now + (hit.maxAge || 0) 231 | } 232 | } 233 | }, this).toArray().filter(function (h) { 234 | return h 235 | }) 236 | } 237 | 238 | LRUCache.prototype.dumpLru = function () { 239 | return this[LRU_LIST] 240 | } 241 | 242 | // LRUCache.prototype.inspect = function (n, opts) { 243 | // var str = 'LRUCache {' 244 | // var extras = false 245 | 246 | // var as = this[ALLOW_STALE] 247 | // if (as) { 248 | // str += '\n allowStale: true' 249 | // extras = true 250 | // } 251 | 252 | // var max = this[MAX] 253 | // if (max && max !== Infinity) { 254 | // if (extras) { 255 | // str += ',' 256 | // } 257 | // str += '\n max: ' + util.inspect(max, opts) 258 | // extras = true 259 | // } 260 | 261 | // var maxAge = this[MAX_AGE] 262 | // if (maxAge) { 263 | // if (extras) { 264 | // str += ',' 265 | // } 266 | // str += '\n maxAge: ' + util.inspect(maxAge, opts) 267 | // extras = true 268 | // } 269 | 270 | // var lc = this[LENGTH_CALCULATOR] 271 | // if (lc && lc !== naiveLength) { 272 | // if (extras) { 273 | // str += ',' 274 | // } 275 | // str += '\n length: ' + util.inspect(this[LENGTH], opts) 276 | // extras = true 277 | // } 278 | 279 | // var didFirst = false 280 | // this[LRU_LIST].forEach(function (item) { 281 | // if (didFirst) { 282 | // str += ',\n ' 283 | // } else { 284 | // if (extras) { 285 | // str += ',\n' 286 | // } 287 | // didFirst = true 288 | // str += '\n ' 289 | // } 290 | // var key = util.inspect(item.key).split('\n').join('\n ') 291 | // var val = { value: item.value } 292 | // if (item.maxAge !== maxAge) { 293 | // val.maxAge = item.maxAge 294 | // } 295 | // if (lc !== naiveLength) { 296 | // val.length = item.length 297 | // } 298 | // if (isStale(this, item)) { 299 | // val.stale = true 300 | // } 301 | 302 | // val = util.inspect(val, opts).split('\n').join('\n ') 303 | // str += key + ' => ' + val 304 | // }) 305 | 306 | // if (didFirst || extras) { 307 | // str += '\n' 308 | // } 309 | // str += '}' 310 | 311 | // return str 312 | // } 313 | 314 | LRUCache.prototype.set = function (key, value, maxAge) { 315 | maxAge = maxAge || this[MAX_AGE] 316 | 317 | var now = maxAge ? Date.now() : 0 318 | var len = this[LENGTH_CALCULATOR](value, key) 319 | 320 | if (this[CACHE].has(key)) { 321 | if (len > this[MAX]) { 322 | del(this, this[CACHE].get(key)) 323 | return false 324 | } 325 | 326 | var node = this[CACHE].get(key) 327 | var item = node.value 328 | 329 | // dispose of the old one before overwriting 330 | // split out into 2 ifs for better coverage tracking 331 | if (this[DISPOSE]) { 332 | if (!this[NO_DISPOSE_ON_SET]) { 333 | this[DISPOSE](key, item.value) 334 | } 335 | } 336 | 337 | item.now = now 338 | item.maxAge = maxAge 339 | item.value = value 340 | this[LENGTH] += len - item.length 341 | item.length = len 342 | this.get(key) 343 | trim(this) 344 | return true 345 | } 346 | 347 | var hit = new Entry(key, value, len, now, maxAge) 348 | 349 | // oversized objects fall out of cache automatically. 350 | if (hit.length > this[MAX]) { 351 | if (this[DISPOSE]) { 352 | this[DISPOSE](key, value) 353 | } 354 | return false 355 | } 356 | 357 | this[LENGTH] += hit.length 358 | this[LRU_LIST].unshift(hit) 359 | this[CACHE].set(key, this[LRU_LIST].head) 360 | trim(this) 361 | return true 362 | } 363 | 364 | LRUCache.prototype.has = function (key) { 365 | if (!this[CACHE].has(key)) return false 366 | var hit = this[CACHE].get(key).value 367 | if (isStale(this, hit)) { 368 | return false 369 | } 370 | return true 371 | } 372 | 373 | LRUCache.prototype.get = function (key) { 374 | return get(this, key, true) 375 | } 376 | 377 | LRUCache.prototype.peek = function (key) { 378 | return get(this, key, false) 379 | } 380 | 381 | LRUCache.prototype.pop = function () { 382 | var node = this[LRU_LIST].tail 383 | if (!node) return null 384 | del(this, node) 385 | return node.value 386 | } 387 | 388 | LRUCache.prototype.del = function (key) { 389 | del(this, this[CACHE].get(key)) 390 | } 391 | 392 | LRUCache.prototype.load = function (arr) { 393 | // reset the cache 394 | this.reset() 395 | 396 | var now = Date.now() 397 | // A previous serialized cache has the most recent items first 398 | for (var l = arr.length - 1; l >= 0; l--) { 399 | var hit = arr[l] 400 | var expiresAt = hit.e || 0 401 | if (expiresAt === 0) { 402 | // the item was created without expiration in a non aged cache 403 | this.set(hit.k, hit.v) 404 | } else { 405 | var maxAge = expiresAt - now 406 | // dont add already expired items 407 | if (maxAge > 0) { 408 | this.set(hit.k, hit.v, maxAge) 409 | } 410 | } 411 | } 412 | } 413 | 414 | LRUCache.prototype.prune = function () { 415 | var self = this 416 | this[CACHE].forEach(function (value, key) { 417 | get(self, key, false) 418 | }) 419 | } 420 | 421 | function get (self, key, doUse) { 422 | var node = self[CACHE].get(key) 423 | if (node) { 424 | var hit = node.value 425 | if (isStale(self, hit)) { 426 | del(self, node) 427 | if (!self[ALLOW_STALE]) hit = undefined 428 | } else { 429 | if (doUse) { 430 | self[LRU_LIST].unshiftNode(node) 431 | } 432 | } 433 | if (hit) hit = hit.value 434 | } 435 | return hit 436 | } 437 | 438 | function isStale (self, hit) { 439 | if (!hit || (!hit.maxAge && !self[MAX_AGE])) { 440 | return false 441 | } 442 | var stale = false 443 | var diff = Date.now() - hit.now 444 | if (hit.maxAge) { 445 | stale = diff > hit.maxAge 446 | } else { 447 | stale = self[MAX_AGE] && (diff > self[MAX_AGE]) 448 | } 449 | return stale 450 | } 451 | 452 | function trim (self) { 453 | if (self[LENGTH] > self[MAX]) { 454 | for (var walker = self[LRU_LIST].tail; 455 | self[LENGTH] > self[MAX] && walker !== null;) { 456 | // We know that we're about to delete this one, and also 457 | // what the next least recently used key will be, so just 458 | // go ahead and set it now. 459 | var prev = walker.prev 460 | del(self, walker) 461 | walker = prev 462 | } 463 | } 464 | } 465 | 466 | function del (self, node) { 467 | if (node) { 468 | var hit = node.value 469 | if (self[DISPOSE]) { 470 | self[DISPOSE](hit.key, hit.value) 471 | } 472 | self[LENGTH] -= hit.length 473 | self[CACHE].delete(hit.key) 474 | self[LRU_LIST].removeNode(node) 475 | } 476 | } 477 | 478 | // classy, since V8 prefers predictable objects. 479 | function Entry (key, value, length, now, maxAge) { 480 | this.key = key 481 | this.value = value 482 | this.length = length 483 | this.now = now 484 | this.maxAge = maxAge || 0 485 | } -------------------------------------------------------------------------------- /src/lib/RichTextRenderer.js: -------------------------------------------------------------------------------- 1 | // import ObjectRenderer from 'pixi.js/lib/core/renderers/webgl/utils/ObjectRenderer'; 2 | // import WebGLRenderer from 'pixi.js/lib/core/renderers/webgl/WebGLRenderer'; 3 | // const PIXI = require('pixi.js'); 4 | // import * as PIXI from 'pixi.js'; 5 | const ObjectRenderer = PIXI.ObjectRenderer; 6 | const WebGLRenderer = PIXI.WebGLRenderer; 7 | import generateMultiTextureShader from './generateMultiTextureShader'; 8 | import createIndicesForQuads from './pixi-source/createIndicesForQuads'; 9 | import checkMaxIfStatmentsInShader from './pixi-source/checkMaxIfStatmentsInShader'; 10 | import BatchBuffer from './pixi-source/BatchBuffer'; 11 | // import settings from 'pixi.js/lib/core/settings'; 12 | // import glCore from 'pixi-gl-core'; 13 | import bitTwiddle from 'bit-twiddle'; 14 | 15 | const glCore = PIXI.glCore; 16 | const settings = PIXI.settings; 17 | 18 | let TICK = 0; 19 | let TEXTURE_TICK = 0; 20 | 21 | /** 22 | * Renderer dedicated to drawing and batching sprites. 23 | * 24 | * @class 25 | * @private 26 | * @memberof PIXI 27 | * @extends PIXI.ObjectRenderer 28 | */ 29 | export default class RichTextRenderer extends ObjectRenderer 30 | { 31 | /** 32 | * @param {PIXI.WebGLRenderer} renderer - The renderer this sprite batch works for. 33 | */ 34 | constructor(renderer) 35 | { 36 | super(renderer); 37 | 38 | /** 39 | * Number of values sent in the vertex buffer. 40 | * aVertexPosition(2), aTextureCoord(1), aColor(1), aTextureId(1) = 5 41 | * uShadow(1), uStroke(1), uFill(1), uGamma(1), uShadowColor(1), uStrokeColor(1), uFillColor(1) = 7 42 | * uShadowOffset(2), uShadowEnable(1), uStrokeEnable(1), uFillEnable(1), uShadowBlur = 6 43 | * 44 | * @member {number} 45 | */ 46 | this.vertSize = 18; 47 | 48 | /** 49 | * The size of the vertex information in bytes. 50 | * 51 | * @member {number} 52 | */ 53 | this.vertByteSize = this.vertSize * 4; 54 | 55 | /** 56 | * The number of images in the SpriteRenderer before it flushes. 57 | * 58 | * @member {number} 59 | */ 60 | this.size = settings.SPRITE_BATCH_SIZE; // 2000 is a nice balance between mobile / desktop 61 | 62 | // the total number of bytes in our batch 63 | // let numVerts = this.size * 4 * this.vertByteSize; 64 | 65 | this.buffers = []; 66 | for (let i = 1; i <= bitTwiddle.nextPow2(this.size); i *= 2) 67 | { 68 | this.buffers.push(new BatchBuffer(i * 4 * this.vertByteSize)); 69 | } 70 | 71 | /** 72 | * Holds the indices of the geometry (quads) to draw 73 | * 74 | * @member {Uint16Array} 75 | */ 76 | this.indices = createIndicesForQuads(this.size); 77 | 78 | /** 79 | * The default shaders that is used if a sprite doesn't have a more specific one. 80 | * there is a shader for each number of textures that can be rendererd. 81 | * These shaders will also be generated on the fly as required. 82 | * @member {PIXI.Shader[]} 83 | */ 84 | this.shader = null; 85 | 86 | this.currentIndex = 0; 87 | this.groups = []; 88 | 89 | for (let k = 0; k < this.size; k++) 90 | { 91 | this.groups[k] = { textures: [], textureCount: 0, ids: [], size: 0, start: 0, blend: 0 }; 92 | } 93 | 94 | // this.sprites = []; 95 | 96 | this.meshes = []; 97 | 98 | this.vertexBuffers = []; 99 | this.vaos = []; 100 | 101 | this.vaoMax = 2; 102 | this.vertexCount = 0; 103 | 104 | this.renderer.on('prerender', this.onPrerender, this); 105 | } 106 | 107 | /** 108 | * Sets up the renderer context and necessary buffers. 109 | * 110 | * @private 111 | */ 112 | onContextChange() 113 | { 114 | const gl = this.renderer.gl; 115 | 116 | if (this.renderer.legacy) 117 | { 118 | this.MAX_TEXTURES = 1; 119 | } 120 | else 121 | { 122 | // step 1: first check max textures the GPU can handle. 123 | this.MAX_TEXTURES = Math.min(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS), settings.SPRITE_MAX_TEXTURES); 124 | 125 | // step 2: check the maximum number of if statements the shader can have too.. 126 | this.MAX_TEXTURES = checkMaxIfStatmentsInShader(this.MAX_TEXTURES, gl); 127 | } 128 | 129 | this.shader = generateMultiTextureShader(gl, this.MAX_TEXTURES); 130 | 131 | // create a couple of buffers 132 | this.indexBuffer = glCore.GLBuffer.createIndexBuffer(gl, this.indices, gl.STATIC_DRAW); 133 | 134 | // we use the second shader as the first one depending on your browser may omit aTextureId 135 | // as it is not used by the shader so is optimized out. 136 | 137 | this.renderer.bindVao(null); 138 | 139 | const attrs = this.shader.attributes; 140 | 141 | for (let i = 0; i < this.vaoMax; i++) 142 | { 143 | /* eslint-disable max-len */ 144 | const vertexBuffer = this.vertexBuffers[i] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); 145 | /* eslint-enable max-len */ 146 | 147 | // build the vao object that will render.. 148 | const vao = this.renderer.createVao() 149 | .addIndex(this.indexBuffer) 150 | .addAttribute(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) 151 | .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) 152 | .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); 153 | 154 | if (attrs.aTextureId) 155 | { 156 | vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); 157 | } 158 | 159 | vao.addAttribute(vertexBuffer, attrs.aShadow, gl.FLOAT, true, this.vertByteSize, 5 * 4) 160 | .addAttribute(vertexBuffer, attrs.aStroke, gl.FLOAT, true, this.vertByteSize, 6 * 4) 161 | .addAttribute(vertexBuffer, attrs.aFill, gl.FLOAT, true, this.vertByteSize, 7 * 4) 162 | .addAttribute(vertexBuffer, attrs.aGamma, gl.FLOAT, true, this.vertByteSize, 8 * 4) 163 | .addAttribute(vertexBuffer, attrs.aShadowColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 9 * 4) 164 | .addAttribute(vertexBuffer, attrs.aStrokeColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 10 * 4) 165 | .addAttribute(vertexBuffer, attrs.aFillColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 11 * 4) 166 | .addAttribute(vertexBuffer, attrs.aShadowOffset, gl.FLOAT, true, this.vertByteSize, 12 * 4) 167 | .addAttribute(vertexBuffer, attrs.aShadowEnable, gl.FLOAT, true, this.vertByteSize, 14 * 4) 168 | .addAttribute(vertexBuffer, attrs.aStrokeEnable, gl.FLOAT, true, this.vertByteSize, 15 * 4) 169 | .addAttribute(vertexBuffer, attrs.aFillEnable, gl.FLOAT, true, this.vertByteSize, 16 * 4) 170 | .addAttribute(vertexBuffer, attrs.aShadowBlur, gl.FLOAT, true, this.vertByteSize, 17 * 4); 171 | 172 | this.vaos[i] = vao; 173 | } 174 | 175 | this.vao = this.vaos[0]; 176 | this.currentBlendMode = 99999; 177 | 178 | this.boundTextures = new Array(this.MAX_TEXTURES); 179 | } 180 | 181 | /** 182 | * Called before the renderer starts rendering. 183 | * 184 | */ 185 | onPrerender() 186 | { 187 | this.vertexCount = 0; 188 | } 189 | 190 | /** 191 | * Renders the sprite object. 192 | * 193 | * @param {PIXI.Sprite} sprite - the sprite to render when using this spritebatch 194 | */ 195 | render(sprite) 196 | { 197 | // TODO set blend modes.. 198 | // check texture.. 199 | // if (this.currentIndex >= this.size) 200 | // { 201 | // this.flush(); 202 | // } 203 | 204 | // get the uvs for the texture 205 | 206 | // if the uvs have not updated then no point rendering just yet! 207 | // if (!sprite._texture._uvs) 208 | // { 209 | // return; 210 | // } 211 | 212 | // push a texture. 213 | // increment the batchsize 214 | // this.sprites[this.currentIndex++] = sprite; 215 | // this.currentIndex--; 216 | 217 | if (this.currentIndex + sprite.vertexList.length < this.size) { 218 | this.meshes.push(...sprite.vertexList); 219 | this.currentIndex += sprite.vertexList.length; 220 | } else { 221 | 222 | let start = 0; 223 | let end = 0; 224 | let leftChars = sprite.vertexList.length; 225 | 226 | while (leftChars > 0) { 227 | if (leftChars > this.size - this.currentIndex - 1) { 228 | start = end; 229 | end += this.size - this.currentIndex - 1; 230 | leftChars -= this.size - this.currentIndex - 1; 231 | } else { 232 | start = end; 233 | end += leftChars; 234 | leftChars = 0; 235 | } 236 | this.meshes.push(...sprite.vertexList.slice(start, end)); 237 | this.currentIndex += end - start; 238 | this.flush(); 239 | } 240 | } 241 | 242 | } 243 | 244 | /** 245 | * Renders the content and empties the current batch. 246 | * 247 | */ 248 | flush() 249 | { 250 | if (this.currentIndex === 0) 251 | { 252 | return; 253 | } 254 | 255 | const gl = this.renderer.gl; 256 | const MAX_TEXTURES = this.MAX_TEXTURES; 257 | 258 | const np2 = bitTwiddle.nextPow2(this.currentIndex); 259 | const log2 = bitTwiddle.log2(np2); 260 | const buffer = this.buffers[log2]; 261 | 262 | // const sprites = this.sprites; 263 | const meshes = this.meshes; 264 | const groups = this.groups; 265 | 266 | const float32View = buffer.float32View; 267 | const uint32View = buffer.uint32View; 268 | 269 | const boundTextures = this.boundTextures; 270 | const rendererBoundTextures = this.renderer.boundTextures; 271 | const touch = this.renderer.textureGC.count; 272 | 273 | let index = 0; 274 | let nextTexture; 275 | let currentTexture; 276 | let groupCount = 1; 277 | let textureCount = 0; 278 | let currentGroup = groups[0]; 279 | let vertexData; 280 | let uvs; 281 | let blendMode = meshes[0].blendMode; 282 | 283 | currentGroup.textureCount = 0; 284 | currentGroup.start = 0; 285 | currentGroup.blend = blendMode; 286 | 287 | TICK++; 288 | 289 | let i; 290 | 291 | // copy textures.. 292 | for (i = 0; i < MAX_TEXTURES; ++i) 293 | { 294 | boundTextures[i] = rendererBoundTextures[i]; 295 | boundTextures[i]._virtalBoundId = i; 296 | } 297 | 298 | for (i = 0; i < this.currentIndex; ++i) 299 | { 300 | // upload the sprite elemetns... 301 | // they have all ready been calculated so we just need to push them into the buffer. 302 | const mesh = meshes[i]; 303 | 304 | nextTexture = mesh.baseTexture; 305 | 306 | if (blendMode !== mesh.blendMode) 307 | { 308 | // finish a group.. 309 | blendMode = mesh.blendMode; 310 | 311 | // force the batch to break! 312 | currentTexture = null; 313 | textureCount = MAX_TEXTURES; 314 | TICK++; 315 | } 316 | 317 | if (currentTexture !== nextTexture) 318 | { 319 | currentTexture = nextTexture; 320 | 321 | if (nextTexture._enabled !== TICK) 322 | { 323 | if (textureCount === MAX_TEXTURES) 324 | { 325 | TICK++; 326 | 327 | currentGroup.size = i - currentGroup.start; 328 | 329 | textureCount = 0; 330 | 331 | currentGroup = groups[groupCount++]; 332 | currentGroup.blend = blendMode; 333 | currentGroup.textureCount = 0; 334 | currentGroup.start = i; 335 | } 336 | 337 | nextTexture.touched = touch; 338 | 339 | if (nextTexture._virtalBoundId === -1) 340 | { 341 | for (let j = 0; j < MAX_TEXTURES; ++j) 342 | { 343 | const tIndex = (j + TEXTURE_TICK) % MAX_TEXTURES; 344 | 345 | const t = boundTextures[tIndex]; 346 | 347 | if (t._enabled !== TICK) 348 | { 349 | TEXTURE_TICK++; 350 | 351 | t._virtalBoundId = -1; 352 | 353 | nextTexture._virtalBoundId = tIndex; 354 | 355 | boundTextures[tIndex] = nextTexture; 356 | break; 357 | } 358 | } 359 | } 360 | 361 | nextTexture._enabled = TICK; 362 | 363 | currentGroup.textureCount++; 364 | currentGroup.ids[textureCount] = nextTexture._virtalBoundId; 365 | currentGroup.textures[textureCount++] = nextTexture; 366 | } 367 | } 368 | 369 | vertexData = mesh.vertexData; 370 | 371 | // TODO this sum does not need to be set each frame.. 372 | uvs = mesh.uvs; 373 | 374 | const interval = this.vertSize; 375 | 376 | if (this.renderer.roundPixels) 377 | { 378 | const resolution = this.renderer.resolution; 379 | 380 | // xy 381 | float32View[index] = ((vertexData[0] * resolution) | 0) / resolution; 382 | float32View[index + 1] = ((vertexData[1] * resolution) | 0) / resolution; 383 | 384 | // xy 385 | float32View[index + interval * 1] = ((vertexData[2] * resolution) | 0) / resolution; 386 | float32View[index + 1 + interval * 1] = ((vertexData[3] * resolution) | 0) / resolution; 387 | 388 | // xy 389 | float32View[index + interval * 2] = ((vertexData[4] * resolution) | 0) / resolution; 390 | float32View[index + 1 + interval * 2] = ((vertexData[5] * resolution) | 0) / resolution; 391 | 392 | // xy 393 | float32View[index + interval * 3] = ((vertexData[6] * resolution) | 0) / resolution; 394 | float32View[index + 1 + interval * 3] = ((vertexData[7] * resolution) | 0) / resolution; 395 | } 396 | else 397 | { 398 | // xy 399 | float32View[index] = vertexData[0]; 400 | float32View[index + 1] = vertexData[1]; 401 | 402 | // xy 403 | float32View[index + interval * 1] = vertexData[2]; 404 | float32View[index + 1 + interval * 1] = vertexData[3]; 405 | 406 | // xy 407 | float32View[index + interval * 2] = vertexData[4]; 408 | float32View[index + 1 + interval * 2] = vertexData[5]; 409 | 410 | // xy 411 | float32View[index + interval * 3] = vertexData[6]; 412 | float32View[index + 1 + interval * 3] = vertexData[7]; 413 | } 414 | 415 | uint32View[index + 2] = uvs[0]; 416 | uint32View[index + 2 + interval * 1] = uvs[1]; 417 | uint32View[index + 2 + interval * 2] = uvs[2]; 418 | uint32View[index + 2 + interval * 3] = uvs[3]; 419 | 420 | /* eslint-disable max-len */ 421 | uint32View[index + 3] = uint32View[index + 3 + interval * 1] = uint32View[index + 3 + interval * 2] = uint32View[index + 3 + interval * 3] = mesh.tintRGB + (Math.min(mesh.worldAlpha, 1) * 255 << 24); 422 | 423 | float32View[index + 4] = float32View[index + 4 + interval * 1] = float32View[index + 4 + interval * 2] = float32View[index + 4 + interval * 3] = nextTexture._virtalBoundId; 424 | 425 | // 426 | float32View[index + 5] = float32View[index + 5 + interval * 1] = float32View[index + 5 + interval * 2] = float32View[index + 5 + interval * 3] = mesh.shadow; 427 | float32View[index + 6] = float32View[index + 6 + interval * 1] = float32View[index + 6 + interval * 2] = float32View[index + 6 + interval * 3] = mesh.stroke; 428 | float32View[index + 7] = float32View[index + 7 + interval * 1] = float32View[index + 7 + interval * 2] = float32View[index + 7 + interval * 3] = mesh.fill; 429 | float32View[index + 8] = float32View[index + 8 + interval * 1] = float32View[index + 8 + interval * 2] = float32View[index + 8 + interval * 3] = mesh.gamma; 430 | uint32View[index + 9] = uint32View[index + 9 + interval * 1] = uint32View[index + 9 + interval * 2] = uint32View[index + 9 + interval * 3] = mesh.shadowColor; 431 | uint32View[index + 10] = uint32View[index + 10 + interval * 1] = uint32View[index + 10 + interval * 2] = uint32View[index + 10 + interval * 3] = mesh.strokeColor; 432 | uint32View[index + 11] = uint32View[index + 11 + interval * 1] = uint32View[index + 11 + interval * 2] = uint32View[index + 11 + interval * 3] = mesh.fillColor; 433 | // float32View[index + 12] = float32View[index + 12 + interval * 1] = float32View[index + 12 + interval * 2] = float32View[index + 12 + interval * 3] = mesh.strokeColor[0]; 434 | // float32View[index + 13] = float32View[index + 13 + interval * 1] = float32View[index + 13 + interval * 2] = float32View[index + 13 + interval * 3] = mesh.strokeColor[1]; 435 | // float32View[index + 14] = float32View[index + 14 + interval * 1] = float32View[index + 14 + interval * 2] = float32View[index + 14 + interval * 3] = mesh.strokeColor[2]; 436 | // float32View[index + 15] = float32View[index + 15 + interval * 1] = float32View[index + 15 + interval * 2] = float32View[index + 15 + interval * 3] = mesh.fillColor[0]; 437 | // float32View[index + 16] = float32View[index + 16 + interval * 1] = float32View[index + 16 + interval * 2] = float32View[index + 16 + interval * 3] = mesh.fillColor[1]; 438 | // float32View[index + 17] = float32View[index + 17 + interval * 1] = float32View[index + 17 + interval * 2] = float32View[index + 17 + interval * 3] = mesh.fillColor[2]; 439 | float32View[index + 12] = float32View[index + 12 + interval * 1] = float32View[index + 12 + interval * 2] = float32View[index + 12 + interval * 3] = mesh.shadowOffset[0]; 440 | float32View[index + 13] = float32View[index + 13 + interval * 1] = float32View[index + 13 + interval * 2] = float32View[index + 13 + interval * 3] = mesh.shadowOffset[1]; 441 | float32View[index + 14] = float32View[index + 14 + interval * 1] = float32View[index + 14 + interval * 2] = float32View[index + 14 + interval * 3] = mesh.shadowEnable; 442 | float32View[index + 15] = float32View[index + 15 + interval * 1] = float32View[index + 15 + interval * 2] = float32View[index + 15 + interval * 3] = mesh.strokeEnable; 443 | float32View[index + 16] = float32View[index + 16 + interval * 1] = float32View[index + 16 + interval * 2] = float32View[index + 16 + interval * 3] = mesh.fillEnable; 444 | float32View[index + 17] = float32View[index + 17 + interval * 1] = float32View[index + 17 + interval * 2] = float32View[index + 17 + interval * 3] = mesh.shadowBlur; 445 | /* eslint-enable max-len */ 446 | 447 | // for (let i = 0; i < 16; i++) { 448 | // float32View[index + 20 + i] = 0.8 449 | // } 450 | 451 | index += this.vertByteSize; 452 | } 453 | 454 | currentGroup.size = i - currentGroup.start; 455 | 456 | if (!settings.CAN_UPLOAD_SAME_BUFFER) 457 | { 458 | // this is still needed for IOS performance.. 459 | // it really does not like uploading to the same buffer in a single frame! 460 | if (this.vaoMax <= this.vertexCount) 461 | { 462 | this.vaoMax++; 463 | 464 | const attrs = this.shader.attributes; 465 | 466 | /* eslint-disable max-len */ 467 | const vertexBuffer = this.vertexBuffers[this.vertexCount] = glCore.GLBuffer.createVertexBuffer(gl, null, gl.STREAM_DRAW); 468 | /* eslint-enable max-len */ 469 | 470 | // build the vao object that will render.. 471 | const vao = this.renderer.createVao() 472 | .addIndex(this.indexBuffer) 473 | .addAttribute(vertexBuffer, attrs.aVertexPosition, gl.FLOAT, false, this.vertByteSize, 0) 474 | .addAttribute(vertexBuffer, attrs.aTextureCoord, gl.UNSIGNED_SHORT, true, this.vertByteSize, 2 * 4) 475 | .addAttribute(vertexBuffer, attrs.aColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 3 * 4); 476 | 477 | if (attrs.aTextureId) 478 | { 479 | vao.addAttribute(vertexBuffer, attrs.aTextureId, gl.FLOAT, false, this.vertByteSize, 4 * 4); 480 | } 481 | 482 | vao.addAttribute(vertexBuffer, attrs.aShadow, gl.FLOAT, true, this.vertByteSize, 5 * 4) 483 | .addAttribute(vertexBuffer, attrs.aStroke, gl.FLOAT, true, this.vertByteSize, 6 * 4) 484 | .addAttribute(vertexBuffer, attrs.aFill, gl.FLOAT, true, this.vertByteSize, 7 * 4) 485 | .addAttribute(vertexBuffer, attrs.aGamma, gl.FLOAT, true, this.vertByteSize, 8 * 4) 486 | .addAttribute(vertexBuffer, attrs.aShadowColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 9 * 4) 487 | .addAttribute(vertexBuffer, attrs.aStrokeColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 10 * 4) 488 | .addAttribute(vertexBuffer, attrs.aFillColor, gl.UNSIGNED_BYTE, true, this.vertByteSize, 11 * 4) 489 | .addAttribute(vertexBuffer, attrs.aShadowOffset, gl.FLOAT, true, this.vertByteSize, 12 * 4) 490 | .addAttribute(vertexBuffer, attrs.aShadowEnable, gl.FLOAT, true, this.vertByteSize, 14 * 4) 491 | .addAttribute(vertexBuffer, attrs.aStrokeEnable, gl.FLOAT, true, this.vertByteSize, 15 * 4) 492 | .addAttribute(vertexBuffer, attrs.aFillEnable, gl.FLOAT, true, this.vertByteSize, 16 * 4) 493 | .addAttribute(vertexBuffer, attrs.aShadowBlur, gl.FLOAT, true, this.vertByteSize, 17 * 4); 494 | 495 | this.vaos[this.vertexCount] = vao; 496 | } 497 | 498 | this.renderer.bindVao(this.vaos[this.vertexCount]); 499 | 500 | this.vertexBuffers[this.vertexCount].upload(buffer.vertices, 0, false); 501 | 502 | this.vertexCount++; 503 | } 504 | else 505 | { 506 | // lets use the faster option, always use buffer number 0 507 | this.vertexBuffers[this.vertexCount].upload(buffer.vertices, 0, true); 508 | } 509 | 510 | for (i = 0; i < MAX_TEXTURES; ++i) 511 | { 512 | rendererBoundTextures[i]._virtalBoundId = -1; 513 | } 514 | 515 | // render the groups.. 516 | for (i = 0; i < groupCount; ++i) 517 | { 518 | const group = groups[i]; 519 | const groupTextureCount = group.textureCount; 520 | 521 | for (let j = 0; j < groupTextureCount; j++) 522 | { 523 | currentTexture = group.textures[j]; 524 | 525 | // reset virtual ids.. 526 | // lets do a quick check.. 527 | if (rendererBoundTextures[group.ids[j]] !== currentTexture) 528 | { 529 | this.renderer.bindTexture(currentTexture, group.ids[j], true); 530 | } 531 | 532 | // reset the virtualId.. 533 | currentTexture._virtalBoundId = -1; 534 | } 535 | 536 | // set the blend mode.. 537 | this.renderer.state.setBlendMode(group.blend); 538 | 539 | gl.drawElements(gl.TRIANGLES, group.size * 6, gl.UNSIGNED_SHORT, group.start * 6 * 2); 540 | } 541 | 542 | // reset elements for the next flush 543 | this.currentIndex = 0; 544 | this.meshes = []; 545 | } 546 | 547 | /** 548 | * Starts a new sprite batch. 549 | */ 550 | start() 551 | { 552 | this.renderer.bindShader(this.shader); 553 | 554 | if (settings.CAN_UPLOAD_SAME_BUFFER) 555 | { 556 | // bind buffer #0, we don't need others 557 | this.renderer.bindVao(this.vaos[this.vertexCount]); 558 | 559 | this.vertexBuffers[this.vertexCount].bind(); 560 | } 561 | } 562 | 563 | /** 564 | * Stops and flushes the current batch. 565 | * 566 | */ 567 | stop() 568 | { 569 | this.flush(); 570 | } 571 | 572 | /** 573 | * Destroys the SpriteRenderer. 574 | * 575 | */ 576 | destroy() 577 | { 578 | for (let i = 0; i < this.vaoMax; i++) 579 | { 580 | if (this.vertexBuffers[i]) 581 | { 582 | this.vertexBuffers[i].destroy(); 583 | } 584 | if (this.vaos[i]) 585 | { 586 | this.vaos[i].destroy(); 587 | } 588 | } 589 | 590 | if (this.indexBuffer) 591 | { 592 | this.indexBuffer.destroy(); 593 | } 594 | 595 | this.renderer.off('prerender', this.onPrerender, this); 596 | 597 | super.destroy(); 598 | 599 | if (this.shader) 600 | { 601 | this.shader.destroy(); 602 | this.shader = null; 603 | } 604 | 605 | this.vertexBuffers = null; 606 | this.vaos = null; 607 | this.indexBuffer = null; 608 | this.indices = null; 609 | 610 | // this.sprites = null; 611 | 612 | this.meshes = null; 613 | 614 | for (let i = 0; i < this.buffers.length; ++i) 615 | { 616 | this.buffers[i].destroy(); 617 | } 618 | } 619 | } 620 | 621 | WebGLRenderer.registerPlugin('richtext', RichTextRenderer); --------------------------------------------------------------------------------