├── .gitignore
├── Sawarabi_Gothic
├── SawarabiGothic-Regular.ttf
└── OFL.txt
├── package.json
├── style.css
├── index.html
├── favicon.svg
├── main.js
├── Canvas.js
└── VerticalTextbox.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/Sawarabi_Gothic/SawarabiGothic-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thuytv-scuti/fabric-CJK-vertical/HEAD/Sawarabi_Gothic/SawarabiGothic-Regular.ttf
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-fabric",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "vite preview"
8 | },
9 | "devDependencies": {
10 | "vite": "^2.7.2"
11 | },
12 | "dependencies": {
13 | "fabric": "^4.6.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | canvas {
7 | border: 1px dashed;
8 | }
9 | #app {
10 | font-family: Avenir, Helvetica, Arial, sans-serif;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | text-align: center;
14 | color: #2c3e50;
15 | margin-top: 60px;
16 | }
17 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 | Vite App
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | import './style.css'
2 | import { fabric } from 'fabric'
3 | import VerticalTextbox from './VerticalTextbox';
4 | import Canvas from './Canvas';
5 |
6 | const canvas = new Canvas('c');
7 | const btnFlip = document.getElementById('ButtonFlip');
8 | // const text = '熊玩\nヌ日池」極健リ\nabc\nhello))健 1234 名8食ー教策12ぜ'
9 | const text = '(abc)こbra\ncket]日「ム」\n極ー右';
10 |
11 | let style = {
12 | "fill": "#292929",
13 | "editable": true,
14 | "fontSize": 40,
15 | width: 300,
16 | "fontWeight": "normal",
17 | "underline": false,
18 | "backgroundColor": "transparent",
19 | "fontFamily": "gothic",
20 | "left": 100,
21 | "top": 50,
22 | lineHeight: 5,
23 | linethrough: false,
24 | overline: false,
25 | };
26 |
27 | // let funcName = 'calcTextWidth';
28 | // let cls = 'IText';
29 |
30 | // fabric[cls].prototype[funcName] = ((originfc) => {
31 | // return function () {
32 | // const result = originfc.apply(this, arguments);
33 | // console.log({ result }, this.width)
34 | // return result;
35 | // }
36 | // })(fabric[cls].prototype[funcName])
37 |
38 | const cjkText = new VerticalTextbox(text, style);
39 | const textbox = new fabric.Textbox(text, Object.assign(style, {
40 | left: 500
41 | }));
42 |
43 | function handleTextFlipped(txtbox, originTxtBox) {
44 | const originIndex = canvas.getObjects().indexOf(originTxtBox);
45 | canvas.startEditing();
46 | canvas.insertAt(txtbox, originIndex, true);
47 | canvas.stopEditing();
48 | canvas.setActiveObject(txtbox);
49 | }
50 |
51 | btnFlip.onclick = () => {
52 | const activeObject = canvas.getActiveObject();
53 | console.log('[x] active-objects', activeObject);
54 |
55 | if (activeObject.type === 'vertical-textbox') {
56 | activeObject.toTextbox(txtbox => handleTextFlipped(txtbox, activeObject))
57 | } else if (activeObject.type === 'textbox') {
58 | VerticalTextbox.fromTextbox(activeObject, txtbox => handleTextFlipped(txtbox, activeObject))
59 | }
60 | }
61 |
62 | canvas.add(cjkText)
63 | // canvas.add(textbox)
64 |
65 | function updateStyles() {
66 | if (cjkText.isEditing) {
67 | cjkText.setSelectionStyles(style)
68 | }
69 |
70 | if (textbox.isEditing) {
71 | textbox.setSelectionStyles(style)
72 | }
73 | }
74 |
75 |
76 | window.addEventListener('keydown', (kbEvt) => {
77 | if (kbEvt.ctrlKey) {
78 | let isHandled = false;
79 |
80 | if (kbEvt.code === 'KeyZ') {
81 | if (kbEvt.shiftKey) {
82 | canvas.redo();
83 | } else {
84 | canvas.undo();
85 | }
86 | }
87 | // style.fontFamily = 'gothic'
88 | // style.fontSize = 50;
89 | // style.linethrough = true;
90 | // style.overline = true;
91 | if (kbEvt.code === 'KeyB') {
92 | style.fontWeight = style.fontWeight === 'bold' ? 'normal' : 'bold';
93 | updateStyles();
94 | isHandled = true;
95 | }
96 |
97 | if (kbEvt.code === 'Digit0') {
98 | style.textBackgroundColor = '#' + Math.floor(Math.random() * 16777215).toString(16);
99 | updateStyles();
100 | isHandled = true;
101 | }
102 |
103 | if (kbEvt.code === 'KeyU') {
104 | style.underline = !style.underline;
105 | updateStyles();
106 | isHandled = true;
107 | }
108 | if (kbEvt.code === 'KeyG') {
109 | style.linethrough = !style.linethrough;
110 | updateStyles();
111 | isHandled = true;
112 | }
113 | if (kbEvt.code === 'KeyE') {
114 | style.overline = !style.overline;
115 | updateStyles();
116 | isHandled = true;
117 | }
118 |
119 | if (kbEvt.code === 'Equal') {
120 | style.fontSize += 2;
121 | updateStyles();
122 | isHandled = true;
123 | }
124 | if (kbEvt.code === 'Minus') {
125 | style.fontSize -= 2;
126 | updateStyles();
127 | isHandled = true;
128 | }
129 | if (isHandled) {
130 | kbEvt.preventDefault();
131 | kbEvt.stopPropagation();
132 | }
133 | canvas.requestRenderAll();
134 | }
135 |
136 | })
137 |
--------------------------------------------------------------------------------
/Sawarabi_Gothic/OFL.txt:
--------------------------------------------------------------------------------
1 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
2 | This license is copied below, and is also available with a FAQ at:
3 | http://scripts.sil.org/OFL
4 |
5 |
6 | -----------------------------------------------------------
7 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
8 | -----------------------------------------------------------
9 |
10 | PREAMBLE
11 | The goals of the Open Font License (OFL) are to stimulate worldwide
12 | development of collaborative font projects, to support the font creation
13 | efforts of academic and linguistic communities, and to provide a free and
14 | open framework in which fonts may be shared and improved in partnership
15 | with others.
16 |
17 | The OFL allows the licensed fonts to be used, studied, modified and
18 | redistributed freely as long as they are not sold by themselves. The
19 | fonts, including any derivative works, can be bundled, embedded,
20 | redistributed and/or sold with any software provided that any reserved
21 | names are not used by derivative works. The fonts and derivatives,
22 | however, cannot be released under any other type of license. The
23 | requirement for fonts to remain under this license does not apply
24 | to any document created using the fonts or their derivatives.
25 |
26 | DEFINITIONS
27 | "Font Software" refers to the set of files released by the Copyright
28 | Holder(s) under this license and clearly marked as such. This may
29 | include source files, build scripts and documentation.
30 |
31 | "Reserved Font Name" refers to any names specified as such after the
32 | copyright statement(s).
33 |
34 | "Original Version" refers to the collection of Font Software components as
35 | distributed by the Copyright Holder(s).
36 |
37 | "Modified Version" refers to any derivative made by adding to, deleting,
38 | or substituting -- in part or in whole -- any of the components of the
39 | Original Version, by changing formats or by porting the Font Software to a
40 | new environment.
41 |
42 | "Author" refers to any designer, engineer, programmer, technical
43 | writer or other person who contributed to the Font Software.
44 |
45 | PERMISSION & CONDITIONS
46 | Permission is hereby granted, free of charge, to any person obtaining
47 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
48 | redistribute, and sell modified and unmodified copies of the Font
49 | Software, subject to the following conditions:
50 |
51 | 1) Neither the Font Software nor any of its individual components,
52 | in Original or Modified Versions, may be sold by itself.
53 |
54 | 2) Original or Modified Versions of the Font Software may be bundled,
55 | redistributed and/or sold with any software, provided that each copy
56 | contains the above copyright notice and this license. These can be
57 | included either as stand-alone text files, human-readable headers or
58 | in the appropriate machine-readable metadata fields within text or
59 | binary files as long as those fields can be easily viewed by the user.
60 |
61 | 3) No Modified Version of the Font Software may use the Reserved Font
62 | Name(s) unless explicit written permission is granted by the corresponding
63 | Copyright Holder. This restriction only applies to the primary font name as
64 | presented to the users.
65 |
66 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
67 | Software shall not be used to promote, endorse or advertise any
68 | Modified Version, except to acknowledge the contribution(s) of the
69 | Copyright Holder(s) and the Author(s) or with their explicit written
70 | permission.
71 |
72 | 5) The Font Software, modified or unmodified, in part or in whole,
73 | must be distributed entirely under this license, and must not be
74 | distributed under any other license. The requirement for fonts to
75 | remain under this license does not apply to any document created
76 | using the Font Software.
77 |
78 | TERMINATION
79 | This license becomes null and void if any of the above conditions are
80 | not met.
81 |
82 | DISCLAIMER
83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
87 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
91 | OTHER DEALINGS IN THE FONT SOFTWARE.
92 |
--------------------------------------------------------------------------------
/Canvas.js:
--------------------------------------------------------------------------------
1 | import { fabric } from "fabric";
2 | import VerticalTextbox from "./VerticalTextbox";
3 |
4 | export default class Canvas extends fabric.Canvas {
5 | constructor(...args) {
6 | super(...args);
7 | fabric['VerticalTextbox'] = VerticalTextbox;
8 | this.isEditing = false;
9 | }
10 |
11 | onStartEditing() { }
12 | onStopEditing() { }
13 |
14 | startEditing() {
15 | this.isEditing = true;
16 | this.onStartEditing();
17 | }
18 |
19 | stopEditing() {
20 | this.isEditing = false;
21 | this.onStopEditing();
22 | this._historySaveAction();
23 | }
24 |
25 | initialize(...args) {
26 | super.initialize.apply(this, args);
27 | this._historyInit();
28 | return this;
29 | }
30 |
31 | dispose(...args) {
32 | super.dispose.apply(this, args);
33 | this._historyDispose();
34 |
35 | return this;
36 | }
37 |
38 | /**
39 | * Returns current state of the string of the canvas
40 | */
41 | _historyNext() {
42 | return JSON.stringify(this.toDatalessJSON(this.extraProps));
43 | }
44 |
45 | /**
46 | * Returns an object with fabricjs event mappings
47 | */
48 | _historyEvents() {
49 | return {
50 | 'object:added': this._historySaveAction,
51 | 'object:removed': this._historySaveAction,
52 | 'object:modified': this._historySaveAction,
53 | 'object:skewing': this._historySaveAction
54 | }
55 | }
56 |
57 | /**
58 | * Initialization of the plugin
59 | */
60 | _historyInit() {
61 | this.historyUndo = [];
62 | this.historyRedo = [];
63 | this.extraProps = ['selectable'];
64 | this.historyNextState = this._historyNext();
65 |
66 | this.on(this._historyEvents());
67 | }
68 |
69 | /**
70 | * Remove the custom event listeners
71 | */
72 | _historyDispose() {
73 | this.off(this._historyEvents())
74 | }
75 |
76 | /**
77 | * It pushes the state of the canvas into history stack
78 | */
79 | _historySaveAction() {
80 |
81 | if (this.historyProcessing || this.isEditing)
82 | return;
83 |
84 | const json = this.historyNextState;
85 | this.historyUndo.push(json);
86 | this.historyNextState = this._historyNext();
87 | this.fire('history:append', { json: json });
88 | }
89 |
90 | /**
91 | * Undo to latest history.
92 | * Pop the latest state of the history. Re-render.
93 | * Also, pushes into redo history.
94 | */
95 | undo(callback) {
96 | // The undo process will render the new states of the objects
97 | // Therefore, object:added and object:modified events will triggered again
98 | // To ignore those events, we are setting a flag.
99 | this.historyProcessing = true;
100 |
101 | const history = this.historyUndo.pop();
102 | if (history) {
103 | // Push the current state to the redo history
104 | this.historyRedo.push(this._historyNext());
105 | this.historyNextState = history;
106 | this._loadHistory(history, 'history:undo', callback);
107 | } else {
108 | this.historyProcessing = false;
109 | }
110 | }
111 |
112 | /**
113 | * Redo to latest undo history.
114 | */
115 | redo(callback) {
116 | // The undo process will render the new states of the objects
117 | // Therefore, object:added and object:modified events will triggered again
118 | // To ignore those events, we are setting a flag.
119 | this.historyProcessing = true;
120 | const history = this.historyRedo.pop();
121 | if (history) {
122 | // Every redo action is actually a new action to the undo history
123 | this.historyUndo.push(this._historyNext());
124 | this.historyNextState = history;
125 | this._loadHistory(history, 'history:redo', callback);
126 | } else {
127 | this.historyProcessing = false;
128 | }
129 | }
130 |
131 | _loadHistory(history, event, callback) {
132 | var that = this;
133 |
134 | this.loadFromJSON(history, function () {
135 | that.renderAll();
136 | that.fire(event);
137 | that.historyProcessing = false;
138 |
139 | if (callback && typeof callback === 'function')
140 | callback();
141 | });
142 | }
143 |
144 | /**
145 | * Clear undo and redo history stacks
146 | */
147 | clearHistory() {
148 | this.historyUndo = [];
149 | this.historyRedo = [];
150 | this.fire('history:clear');
151 | }
152 |
153 | /**
154 | * Off the history
155 | */
156 | offHistory() {
157 | this.historyProcessing = true;
158 | }
159 |
160 | /**
161 | * On the history
162 | */
163 | onHistory() {
164 | this.historyProcessing = false;
165 |
166 | this._historySaveAction();
167 | }
168 | }
--------------------------------------------------------------------------------
/VerticalTextbox.js:
--------------------------------------------------------------------------------
1 | import { fabric } from 'fabric'
2 |
3 | const LATIN_CHARS_REGX = /[a-zA-Z\.\s]+/;
4 | const NUMBERIC_REGX = /[0-9]/;
5 | const BRACKETS_REGX = /[\(\)\]\[\{\}\]]/;
6 | const JP_BRACKETS = /[ー「」『』()〔〕[]{}⦅⦆〈〉《》【】〖〗〘〙〚〛゛゜。、・゠=〜…•‥◦﹅﹆]/;
7 | class VerticalTextbox extends fabric.IText {
8 | initialize(text, options) {
9 | this.textAlign = 'right';
10 | this.direction = 'rtl';
11 | this.type = 'vertical-textbox';
12 | this.typeObject = 'vertical-textbox';
13 | this.minHeight = options.width;
14 |
15 | // re-map keys movements
16 | this.keysMapRtl = Object.assign(this.keysMapRtl, {
17 | 33: 'moveCursorLeft',
18 | 34: 'moveCursorDown',
19 | 35: 'moveCursorUp',
20 | 36: 'moveCursorRight',
21 | 37: 'moveCursorDown',
22 | 38: 'moveCursorLeft',
23 | 39: 'moveCursorUp',
24 | 40: 'moveCursorRight',
25 | });
26 |
27 | this.offsets = {
28 | underline: 0.05,
29 | linethrough: 0.65,
30 | overline: 1.10
31 | };
32 |
33 | return super.initialize.call(this, text, options);
34 | }
35 |
36 | initDimensions() {
37 | super.initDimensions.call(this);
38 |
39 | if (this.height < this.minHeight) {
40 | this._set('height', this.minHeight);
41 | }
42 | }
43 |
44 | static fromObject(object, callback) {
45 | const objectCopy = fabric.util.object.clone(object);
46 | delete objectCopy.path;
47 | return fabric.Object._fromObject('VerticalTextbox', objectCopy, function (textInstance) {
48 | callback(textInstance);
49 | }, 'vertical-textbox');
50 | };
51 |
52 | toTextbox(callback) {
53 | const objectCopy = fabric.util.object.clone(this.toObject());
54 | delete objectCopy.path;
55 | objectCopy.direction = 'ltr';
56 | objectCopy.textAlign = 'left';
57 | delete objectCopy.minHeight;
58 | return fabric.Object._fromObject('Textbox', objectCopy, function (textbox) {
59 | textbox.type = 'textbox';
60 | textbox.typeObject = 'text';
61 | callback(textbox);
62 | }, 'text');
63 | }
64 |
65 | static fromTextbox(textbox, callback) {
66 | const objectCopy = fabric.util.object.clone(textbox.toObject());
67 | delete objectCopy.path;
68 | return fabric.Object._fromObject('VerticalTextbox', objectCopy, function (textInstance) {
69 | textInstance.textAlign = 'right';
70 | textInstance.direction = 'rtl';
71 | textInstance.type = 'vertical-textbox';
72 | textInstance.typeObject = 'vertical-textbox';
73 | callback(textInstance);
74 | }, 'vertical-textbox');
75 | }
76 |
77 | _renderTextCommon(ctx, method) {
78 | ctx.save();
79 | var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset();
80 | for (var i = 0, len = this._textLines.length; i < len; i++) {
81 |
82 | !this.__charBounds[i] && this.measureLine(i);
83 |
84 | this._renderTextLine(
85 | method,
86 | ctx,
87 | this._textLines[i],
88 | left - lineHeights,
89 | top + this._getLineLeftOffset(i),
90 | i
91 | );
92 | lineHeights += this.getHeightOfLine(i);
93 | }
94 | ctx.restore();
95 | }
96 |
97 | _renderCJKChar(method, ctx, lineIndex, charIndex, left, top) {
98 | let charbox = this.__charBounds[lineIndex][charIndex],
99 | char = this._textLines[lineIndex][charIndex],
100 | localLineHeight = this.getHeightOfLine(lineIndex),
101 | charLeft = left - (localLineHeight / this.lineHeight - charbox.width) / 2,
102 | charTop = top + charbox.top + charbox.height - this.lineHeight,
103 | isLtr = this.direction === 'ltr';
104 | ctx.save();
105 | ctx.canvas.setAttribute('dir', isLtr ? 'ltr' : 'rtl');
106 | ctx.direction = isLtr ? 'ltr' : 'rtl';
107 | ctx.textAlign = isLtr ? 'left' : 'right';
108 |
109 | if (JP_BRACKETS.test(char)) {
110 | // TODO: why the fuck do we need plus 3 and minus 5 here...
111 | charTop += this.lineHeight * this._fontSizeMult;
112 | charLeft -= this.lineHeight * this._fontSizeMult;
113 | const tx = charLeft - charbox.width / 2,
114 | ty = charTop - charbox.height / 2; // somehow, the char is a bit higher after rotation;
115 | ctx.translate(tx, ty);
116 | ctx.rotate(-Math.PI / 2);
117 | ctx.translate(-tx, -ty);
118 | }
119 |
120 | this._renderChar(method,
121 | ctx,
122 | lineIndex,
123 | charIndex,
124 | char,
125 | charLeft,
126 | charTop,
127 | 0
128 | );
129 |
130 | ctx.restore();
131 | }
132 |
133 | _renderAlphanumeric(method, ctx, lineIndex, startIndex, endIndex, left, top) {
134 | let charBox = this.__charBounds[lineIndex][startIndex],
135 | chars = '',
136 | drawWidth = 0,
137 | localLineHeight = this.getHeightOfLine(lineIndex),
138 | drawLeft = left,
139 | drawTop = top + charBox.top + charBox.height;
140 |
141 | for (let i = startIndex; i <= endIndex; i++) {
142 | chars += this._textLines[lineIndex][i];
143 | drawWidth += this.__charBounds[lineIndex][i].width;
144 | }
145 | const widthFactor = (drawWidth + localLineHeight / this.lineHeight);
146 | const heightFactor = drawWidth / 2 - charBox.height;
147 | drawLeft = drawLeft - widthFactor / 2;
148 | drawTop = drawTop + heightFactor;
149 | ctx.save();
150 | const _boxHeight = charBox.height;
151 | const tx = drawLeft + drawWidth / 2 - _boxHeight / 8,
152 | ty = drawTop - _boxHeight / 8;
153 | ctx.translate(tx, ty);
154 | ctx.rotate(Math.PI / 2);
155 | ctx.translate(-tx, -ty);
156 | this._renderChar(method,
157 | ctx,
158 | lineIndex,
159 | startIndex,
160 | chars,
161 | drawLeft,
162 | drawTop,
163 | 0
164 | );
165 |
166 | ctx.restore();
167 | }
168 |
169 | _renderChars(method, ctx, line, left, top, lineIndex) {
170 | let timeToRender,
171 | startChar = null,
172 | actualStyle,
173 | nextStyle,
174 | endChar = null;
175 | ctx.save();
176 | for (var i = 0, len = line.length - 1; i <= len; i++) {
177 | if (this._isLatin(line[i])) {
178 | timeToRender = (i === len || !this._isLatin(line[i + 1]));
179 | if (startChar === null && this._isLatin(line[i])) {
180 | startChar = i;
181 | };
182 |
183 | if (!timeToRender) {
184 | actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
185 | nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
186 | timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
187 | }
188 |
189 | if (timeToRender) {
190 | endChar = i;
191 | this._renderAlphanumeric(method, ctx, lineIndex, startChar, endChar, left, top);
192 | timeToRender = false;
193 | startChar = null;
194 | endChar = null;
195 | actualStyle = nextStyle;
196 | }
197 | } else {
198 | this._renderCJKChar(method, ctx, lineIndex, i, left, top);
199 | }
200 | }
201 | ctx.restore();
202 | }
203 |
204 | _isLatin(char) {
205 | return LATIN_CHARS_REGX.test(char) || BRACKETS_REGX.test(char) || NUMBERIC_REGX.test(char);
206 | }
207 |
208 | calcTextWidth() {
209 | return super.calcTextHeight.call(this)
210 | }
211 |
212 | calcTextHeight() {
213 | let longestLine = 0,
214 | currentLineHeight = 0,
215 | char,
216 | charBox,
217 | space = 0;
218 |
219 | if (this.charSpacing !== 0) {
220 | space = this._getWidthOfCharSpacing();
221 | }
222 | for (var lineIndex = 0, len = this._textLines.length; lineIndex < len; lineIndex++) {
223 | !this.__charBounds[lineIndex] && this._measureLine(lineIndex);
224 |
225 | currentLineHeight = 0;
226 | for (let charIndex = 0, rlen = this._textLines[lineIndex].length; charIndex < rlen; charIndex++) {
227 | char = this._textLines[lineIndex][charIndex];
228 | charBox = this.__charBounds[lineIndex][charIndex];
229 | if (char) {
230 | if (this._isLatin(char)) {
231 | currentLineHeight += charBox.width + space;
232 | } else {
233 | currentLineHeight += charBox.height + space;
234 | }
235 | }
236 | }
237 | if (currentLineHeight > longestLine) {
238 | longestLine = currentLineHeight;
239 | }
240 | }
241 | return longestLine + this.cursorWidth;
242 | }
243 |
244 | getSelectionStartFromPointer(e) {
245 | var mouseOffset = this.getLocalPointer(e),
246 | prevHeight = 0,
247 | width = 0,
248 | height = 0,
249 | charIndex = 0,
250 | lineIndex = 0,
251 | charBox,
252 | lineHeight = 0,
253 | space = 0,
254 | line;
255 |
256 | if (this.charSpacing !== 0) {
257 | space = this._getWidthOfCharSpacing();
258 | }
259 | // handling of RTL: in order to get things work correctly,
260 | // we assume RTL writing is mirrored compared to LTR writing.
261 | // so in position detection we mirror the X offset, and when is time
262 | // of rendering it, we mirror it again.
263 | mouseOffset.x = this.width * this.scaleX - mouseOffset.x + width;
264 | for (var i = 0, len = this._textLines.length; i < len; i++) {
265 | if (width <= mouseOffset.x) {
266 | lineHeight = this.getHeightOfLine(i) * this.scaleY;
267 | width += lineHeight;
268 | lineIndex = i;
269 | if (i > 0) {
270 | charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1);
271 | }
272 | }
273 | else {
274 | break;
275 | }
276 | }
277 | line = this._textLines[lineIndex];
278 | for (var j = 0, jlen = line.length; j < jlen; j++) {
279 | prevHeight = height;
280 | charBox = this.__charBounds[lineIndex][j];
281 | if (this._isLatin(this._textLines[lineIndex][j])) {
282 | height += charBox.width * this.scaleY + space;
283 | } else {
284 | height += charBox.height * this.scaleY + space;
285 | }
286 | if (height <= mouseOffset.y) {
287 | charIndex++;
288 | }
289 | else {
290 | break;
291 | }
292 | }
293 |
294 | return this._getNewSelectionStartFromOffset(mouseOffset, prevHeight, height, charIndex, jlen);
295 | }
296 |
297 | _getNewSelectionStartFromOffset(mouseOffset, prevHeight, height, index, jlen) {
298 | // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0
299 | var distanceBtwLastCharAndCursor = mouseOffset.y - prevHeight,
300 | distanceBtwNextCharAndCursor = height - mouseOffset.y,
301 | offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ||
302 | distanceBtwNextCharAndCursor < 0 ? 0 : 1,
303 | newSelectionStart = index + offset;
304 | // if object is horizontally flipped, mirror cursor location from the end
305 | if (this.flipX) {
306 | newSelectionStart = jlen - newSelectionStart;
307 | }
308 |
309 | if (newSelectionStart > this._text.length) {
310 | newSelectionStart = this._text.length;
311 | }
312 |
313 | return newSelectionStart;
314 | }
315 |
316 | _getCursorBoundariesOffsets(position) {
317 | if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
318 | return this.cursorOffsetCache;
319 | }
320 | var lineLeftOffset,
321 | lineIndex,
322 | charIndex,
323 | topOffset = 0,
324 | leftOffset = 0,
325 | boundaries,
326 | charBox,
327 | cursorPosition = this.get2DCursorLocation(position);
328 | charIndex = cursorPosition.charIndex;
329 | lineIndex = cursorPosition.lineIndex;
330 | for (var i = 0; i < lineIndex; i++) {
331 | leftOffset += this.getHeightOfLine(i);
332 | }
333 |
334 | for (var i = 0; i < charIndex; i++) {
335 | charBox = this.__charBounds[lineIndex][i];
336 | if (this._isLatin(this._textLines[lineIndex][i])) {
337 | topOffset += charBox.width;
338 | } else {
339 | topOffset += charBox.height;
340 | }
341 | }
342 |
343 | lineLeftOffset = this._getLineLeftOffset(lineIndex);
344 | // bound && (leftOffset = bound.left);
345 | if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
346 | leftOffset -= this._getWidthOfCharSpacing();
347 | }
348 | boundaries = {
349 | top: lineLeftOffset + (topOffset > 0 ? topOffset : 0),
350 | left: leftOffset,
351 | };
352 | if (this.direction === 'rtl') {
353 | boundaries.left *= -1;
354 | }
355 |
356 | this.cursorOffsetCache = boundaries;
357 | return this.cursorOffsetCache;
358 | }
359 | _getGraphemeBox(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) {
360 | let box = super._getGraphemeBox(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft);
361 | box.top = 0;
362 | box.height = Number(box.height)
363 |
364 | if (charIndex > 0 && !skipLeft) {
365 | const previousBox = this.__charBounds[lineIndex][charIndex - 1];
366 | const isAlphaNumeric = this._isLatin(this._textLines[lineIndex][charIndex - 1]);
367 | box.top = previousBox.top + previousBox[isAlphaNumeric ? 'width' : 'height'];
368 | }
369 |
370 | return box;
371 | }
372 |
373 | /**
374 | *
375 | * @param {*} boundaries
376 | * @param {CanvasRenderingContext2D} ctx
377 | */
378 | renderSelection(boundaries, ctx) {
379 | var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
380 | selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
381 | isJustify = this.textAlign.indexOf('justify') !== -1,
382 | start = this.get2DCursorLocation(selectionStart),
383 | end = this.get2DCursorLocation(selectionEnd),
384 | startLine = start.lineIndex,
385 | endLine = end.lineIndex,
386 | startChar = start.charIndex < 0 ? 0 : start.charIndex,
387 | endChar = end.charIndex < 0 ? 0 : end.charIndex;
388 | for (var i = startLine; i <= endLine; i++) {
389 | var lineHeight = this.getHeightOfLine(i),
390 | boxStart = 0, boxEnd = 0;
391 |
392 | if (i === startLine) {
393 | boxStart = this.__charBounds[startLine][startChar].top;
394 | }
395 | if (i >= startLine && i < endLine) {
396 | boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.height : this.getLineWidth(i) || 5; // WTF is this 5?
397 | }
398 | else if (i === endLine) {
399 | if (endChar === 0) {
400 | boxEnd = this.__charBounds[endLine][endChar].top;
401 | }
402 | else {
403 | var charSpacing = this._getWidthOfCharSpacing();
404 | const prevCharBox = this.__charBounds[endLine][endChar - 1];
405 | boxEnd = prevCharBox.top - charSpacing;
406 | if (this._isLatin(this._textLines[endLine][endChar - 1])) {
407 | boxEnd += prevCharBox.width;
408 | } else {
409 | boxEnd += prevCharBox.height;
410 | }
411 | }
412 | }
413 |
414 | let drawStart = boundaries.left - boundaries.leftOffset,
415 | drawWidth = lineHeight,
416 | drawHeight = boxEnd - boxStart;
417 |
418 | if (this.lineHeight < 1 || (i === endLine && this.lineHeight > 1)) {
419 | drawWidth /= this.lineHeight;
420 | }
421 | if (this.inCompositionMode) {
422 | ctx.fillStyle = this.compositionColor || 'black';
423 | }
424 | else {
425 | ctx.fillStyle = this.selectionColor;
426 | }
427 | if (this.direction === 'rtl') {
428 | drawStart = this.width - drawStart - drawWidth;
429 | }
430 | ctx.fillRect(
431 | drawStart,
432 | boundaries.top + boxStart,
433 | drawWidth,
434 | drawHeight,
435 | );
436 | boundaries.leftOffset -= lineHeight;
437 | }
438 | }
439 |
440 |
441 | renderCursor(boundaries, ctx) {
442 | var cursorLocation = this.get2DCursorLocation(),
443 | lineIndex = cursorLocation.lineIndex,
444 | charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
445 | charBox = this.__charBounds[lineIndex][charIndex],
446 | charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
447 | multiplier = this.scaleX * this.canvas.getZoom(),
448 | cursorWidth = this.cursorWidth / multiplier,
449 | topOffset = boundaries.topOffset,
450 | lineHeight = this.getHeightOfLine(lineIndex),
451 | drawStart = boundaries.left - boundaries.leftOffset + (lineHeight / this.lineHeight + charBox.height) / 2;
452 |
453 | if (this.inCompositionMode) {
454 | this.renderSelection(boundaries, ctx);
455 | }
456 | if (this.direction === 'rtl') {
457 | drawStart = this.width - drawStart;
458 | }
459 | ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
460 | ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
461 | ctx.fillRect(
462 | drawStart,
463 | topOffset + boundaries.top,
464 | charHeight,
465 | cursorWidth,
466 | );
467 | }
468 |
469 |
470 | _renderTextLinesBackground(ctx) {
471 | if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
472 | return;
473 | }
474 | var heightOfLine,
475 | originalFill = ctx.fillStyle,
476 | line, lastColor,
477 | leftOffset = this.width - this._getLeftOffset(),
478 | lineTopOffset = this._getTopOffset(),
479 | charBox, currentColor, path = this.path,
480 | boxHeight = 0,
481 | left = 0,
482 | top = null,
483 | char;
484 |
485 | for (var i = 0, len = this._textLines.length; i < len; i++) {
486 | heightOfLine = this.getHeightOfLine(i);
487 | left += heightOfLine;
488 | if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) {
489 | continue;
490 | }
491 | line = this._textLines[i];
492 | boxHeight = 0;
493 | lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
494 | top = this.__charBounds[i][0].top;
495 | for (var j = 0, jlen = line.length; j < jlen; j++) {
496 | char = line[j];
497 | charBox = this.__charBounds[i][j];
498 | currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
499 |
500 | if (currentColor !== lastColor) {
501 | ctx.fillStyle = lastColor;
502 | if (lastColor) {
503 | ctx.fillRect(
504 | leftOffset - left + heightOfLine - (heightOfLine / this.lineHeight),
505 | lineTopOffset + top,
506 | heightOfLine / this.lineHeight,
507 | boxHeight
508 | )
509 | }
510 |
511 | if (this._isLatin(char)) {
512 | boxHeight = charBox.width;
513 | } else {
514 | boxHeight = charBox.height;
515 | }
516 | lastColor = currentColor;
517 | top = charBox.top;
518 | }
519 | else {
520 | if (this._isLatin(char)) {
521 | boxHeight += charBox.kernedWidth;
522 | } else {
523 | boxHeight += charBox.height;
524 | }
525 | }
526 | }
527 | if (currentColor && !path) {
528 | ctx.fillStyle = currentColor;
529 | ctx.fillRect(
530 | leftOffset - left + heightOfLine - (heightOfLine / this.lineHeight),
531 | lineTopOffset + top,
532 | heightOfLine / this.lineHeight,
533 | boxHeight
534 | );
535 | }
536 |
537 | }
538 | ctx.fillStyle = originalFill;
539 | // if there is text background color no
540 | // other shadows should be casted
541 | this._removeShadow(ctx);
542 | }
543 |
544 | _renderTextDecoration(ctx, type) {
545 | if (!this[type] && !this.styleHas(type)) {
546 | return;
547 | }
548 | let heightOfLine, size, _size,
549 | dy, _dy,
550 | left = 0,
551 | top = 0,
552 | boxHeight = 0,
553 | char = '',
554 | line, lastDecoration,
555 | leftOffset = this.width - this._getLeftOffset(),
556 | topOffset = this._getTopOffset(),
557 | boxWidth, charBox, currentDecoration,
558 | currentFill, lastFill,
559 | offsetY = this.offsets[type];
560 |
561 | for (var i = 0, len = this._textLines.length; i < len; i++) {
562 | heightOfLine = this.getHeightOfLine(i);
563 | left += heightOfLine;
564 | if (!this[type] && !this.styleHas(type, i)) { continue; }
565 |
566 | boxHeight = 0;
567 | line = this._textLines[i];
568 | boxWidth = 0;
569 | lastDecoration = this.getValueOfPropertyAt(i, 0, type);
570 | lastFill = this.getValueOfPropertyAt(i, 0, 'fill');
571 | top = this.__charBounds[i][0].top + this.lineHeight;
572 |
573 | size = heightOfLine / this.lineHeight;
574 | dy = this.getValueOfPropertyAt(i, 0, 'deltaY');
575 | for (var j = 0, jlen = line.length; j < jlen; j++) {
576 | charBox = this.__charBounds[i][j];
577 | char = line[j];
578 | currentDecoration = this.getValueOfPropertyAt(i, j, type);
579 | currentFill = this.getValueOfPropertyAt(i, j, 'fill');
580 | _size = this.getHeightOfChar(i, j);
581 | _dy = this.getValueOfPropertyAt(i, j, 'deltaY');
582 |
583 | (!lastDecoration) && (top = charBox.top);
584 |
585 | if (
586 | (currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy)
587 | && boxWidth > 0
588 | ) {
589 | if (lastDecoration && lastFill) {
590 | ctx.fillStyle = lastFill;
591 | ctx.fillRect(
592 | leftOffset - left + heightOfLine - _size * offsetY,
593 | topOffset + top,
594 | this.fontSize / 15,
595 | boxHeight,
596 | );
597 | }
598 | boxWidth = charBox.width;
599 | if (this._isLatin(char)) {
600 | boxHeight = charBox.width;
601 | } else {
602 | boxHeight = charBox.height;
603 | }
604 | lastDecoration = currentDecoration;
605 | lastFill = currentFill;
606 | size = _size;
607 | dy = _dy;
608 | top = charBox.top;
609 | }
610 | else {
611 | if (this._isLatin(char)) {
612 | boxHeight += charBox.kernedWidth;
613 | } else {
614 | boxHeight += charBox.height;
615 | }
616 | boxWidth += charBox.kernedWidth;
617 | }
618 | }
619 | ctx.fillStyle = currentFill;
620 | if (currentDecoration && currentFill) {
621 | ctx.fillRect(
622 | leftOffset - left + heightOfLine - _size * offsetY,
623 | topOffset + top,
624 | this.fontSize / 15,
625 | boxHeight,
626 | );
627 | }
628 | }
629 | // if there is text background color no
630 | // other shadows should be casted
631 | this._removeShadow(ctx);
632 | }
633 | }
634 |
635 | export default VerticalTextbox;
--------------------------------------------------------------------------------