├── .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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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; --------------------------------------------------------------------------------