├── 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 | = '' tagName:TagName '>' { 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);
--------------------------------------------------------------------------------