摘要:項目中文字由進行渲染。待觸發(fā)時,取消中文輸入標記,將文字渲染到上。而其中一些有趣的細節(jié)實現(xiàn)如文本渲染,對中文筆畫分割實現(xiàn)有趣的動畫等并沒有描寫。
導言
目前富文本編輯器的實現(xiàn)主要有兩種技術方案:一個是利用contenteditable屬性直接對html元素進行編輯,如draft.js;另一種是代理textarea + 自定義div + 模擬光標實現(xiàn)。對于類似"word"的經典富文本編輯器,一般會采用以上兩種技術方案之一,而不會考慮用canvas實現(xiàn)。
事實上,官方最佳實踐中已經特別聲明了不推薦用canvas實現(xiàn)編輯器,詳見https://www.w3.org/TR/2dconte...
不推薦的原因包括光標位置維護、鍵盤移動的實現(xiàn)、以及沒有原生文本輸入處理等等。
既然如此,為何還要用canvas制作文本編輯器呢?這是因為對一些特殊的創(chuàng)作來說,canvas能更好的實現(xiàn)展示需求。比如藝術字效果的渲染,以及文本、背景動畫等。
基于這點想法,便有了“簡詩”這個自娛自樂的小項目。
簡詩是為短詩文創(chuàng)作而開發(fā)的文本編輯器,主要面向中文寫作。中文最特別之處便在于其筆畫,所以在開發(fā)之初,我便想對文字進行處理之時,一定要把漢字進行筆畫分割,以便實現(xiàn)更多有趣的效果的。
項目中文字由WebGL進行渲染。基本思路是先根據用戶選擇的字體,將文字寫在離屏canvas上,然后利用getImageData api獲取文字像素數據,進行連通域查詢、分割、邊緣查找及三角化后,由WebGL進行渲染。
(注:這種處理方式的好處是對任意系統(tǒng)支持的字體都可以實現(xiàn)藝術效果,而無需額外的字體開發(fā)。目前項目中沒有引入字體文件,用到的字體都是Mac內置的字體,Mac用戶如發(fā)現(xiàn)其中有的字體系統(tǒng)沒有默認安裝,只需到“字體冊”中安裝一下即可)
這一系列過程會單開一篇文章來寫,本文主要描述canvas編輯器核心的實現(xiàn)。
實現(xiàn)效果預覽地址:https://moyuer1992.github.io/...
源碼地址:https://github.com/moyuer1992...
用canvas實現(xiàn)編輯器最關鍵的一點就是如何監(jiān)聽鍵盤文字輸入,如果通過鍵盤事件自己處理,英文尚可,中文肯定是不可行的。所以還是需要使用原生textarea做一層代理。
代理textarea輸入框是不可見的。這里需特別注意下,若用display: none隱藏輸入框,則無法觸發(fā)focus事件,所以輸入框需要利用z-index來做隱藏。
當用戶點擊canvas時,程序控制觸發(fā)textarea的focus事件,繼而用戶輸入時,也自然觸發(fā)了textarea的input事件:
var pos = this._convertWindowPosToCanvas(e.clientX, e.clientY); if (pos.x !== -1 && pos.y !== -1) { this.focus(pos.x, pos.y); } else { this.blur(); }
focus (x, y) { var pos = this.findPosfromMap(x, y); this.selection.update(pos.row, pos.col); this.updateCursor(); this.$input.focus(); this.$cursor.css("visibility", "visible"); this.onFocus = true; }中文輸入
按照上述方法,很容易想到處理文本輸入的流程:
監(jiān)聽隱藏輸入框的input事件
觸發(fā)input事件時,將輸入框value取出,渲染到canvas中對應位置
清空輸入框,繼續(xù)監(jiān)聽
然而,當輸入中文時,一些輸入法會出現(xiàn)這種現(xiàn)象:
顯然,當使用中文輸入法鍵入拼音時,拼音字母已經寫入輸入框中,觸發(fā)了input事件,但事實上用戶并沒有鍵入完畢。這就導致了最終拼音字母和漢字全部被寫到了canvas上,這并非我們想要的結果。
如何解決呢?這里需要用到input元素的onCompStart和onCompEnd事件。
當中文輸入開始時,會觸發(fā)onCompStart事件,此時做一個標記,告知程序用戶正在中文輸入,input事件觸發(fā)時,判斷當前是否正在鍵入中文,若是,則不作任何操作。待onCompEnd觸發(fā)時,取消中文輸入標記,將文字渲染到canvas上。
this.$input.on("compositionstart", this.onCompStart.bind(this)); this.$input.on("compositionend", this.onCompEnd.bind(this)); this.$input.on("input", this.onInputChar.bind(this));
onCompStart (e) { this.inputStatus = "CHINESE_TYPING"; } onCompEnd (e) { var that = this; setTimeout(function () { that.input(); that.inputStatus = "CHINESE_TYPE_END"; }, 100) } onInputChar (e) { if (this.inputStatus === "CHINESE_TYPING") { return; } this.inputStatus = "CHAR_TYPING"; this.input(); }虛擬光標
用canvas實現(xiàn)編輯器需要模擬光標,這里用一個div來實現(xiàn),設置position為absolute,用top、left來定位光標位置。
this.$cursor = $(""); this.cursorNode = this.$cursor.get(0); this.$cursor.css("width", "1px"); this.$cursor.css("height", this.style.lineHeight() + "px"); this.$cursor.css("position", "absolute"); this.$cursor.css("top", this.selection.rowIndex * this.style.lineHeight()); this.$cursor.css("left", this.selection.colIndex * this.fontSize); this.$cursor.css("background-color", "black");
用css動畫實現(xiàn)光標1s閃動一次。
@keyframes cursor { from { opacity: 0; } 50% { opacity: 1; } to { opacity: 0; } } .cursor { animation: cursor 1s ease infinite; }
原理雖然簡單,但是隨著文字、排版、用戶操作變更,如何維護光標位置,是一件較為繁瑣的事。
這里定義了Selection類以存儲用戶選擇區(qū)域。未選擇任何文本的情況下,selection位置及為光標所在位置。(目前此項目尚未支持選擇文本功能,但Selection類的設計方式對以后此功能的添加是支持的。)
selection對象中,位置存儲完全是針對文本矩陣的,而非對應屏幕上真正的坐標。項目中另外定義了map矩陣存儲文本位置數據。map的具體設計下面一節(jié)會詳細講到。
更新光標函數如下:
updateCursor () { var pos = this.selection.getSelEndPosition(); this.$cursor.css("height", this.style.lineHeight() + "px"); this.$cursor.css("left", this.map[pos.rowIndex][pos.colIndex].cursorX + "px"); this.$cursor.css("top", this.map[pos.rowIndex][pos.colIndex].cursorY + "px"); }文字排版
上一節(jié)中已經提到,項目中定義了map矩陣存儲文本位置信息。每次渲染文字時,會依據當前樣式(版式、文字大小等)更新map數據。
目前項目支持居中和左對齊兩個版式,map更新時,這兩個版式的位置計算有所不同。
對于左對齊版式,邏輯比較簡單,只要從左邊邊距處開始,逐個寫入文字,直至換行即可。
而對于居中版式,邏輯要稍微復雜一些,處理每段文字時,要先根據每段文字總長度、canvas寬度、邊距大小來確定文字位置。如果此段文字不足一行,則直接居中顯示,若超過一行,將每行填滿后,對不足一行的部分居中顯示。
每個map元素結構如下:
{ char: 對應字符/文字, x: 文字起始x坐標, y: 文字起始y坐標, cursorX: 對應光標x坐標, cursorY: 對應光標y坐標 }動畫精靈
之所以用canvas實現(xiàn)文本編輯器,便是為了藝術效果的渲染以及文字、背景動畫。項目希望實現(xiàn)文字、背景樣式的自由切換,為了降低耦合度,為每種文字、背景樣式多帶帶定義精靈。
文本精靈基類:https://github.com/moyuer1992...
文本精靈文件夾:https://github.com/moyuer1992...
背景精靈基類:https://github.com/moyuer1992...
背景精靈文件夾:https://github.com/moyuer1992...
精靈類中的核心是drawStatic、drawFrame、advance三個方法。
advance函數中,對進入下一幀時需要改變的參數進行定義。
drawStatic用于靜態(tài)效果的渲染。Editor類中,每次需要重新渲染靜態(tài)文字時,都會調用此方法。
_fillText () { if (this.map.length === 1 && this.map[0].length === 1) { this.clearText(); } else { $(".render-tip").addClass("show"); setTimeout(this.textSprite.drawStatic.bind(this.textSprite), 0); } }
drawFrame用于動畫效果每一幀的渲染,當動畫播放時,會逐幀調用此方法。
play () { this.animating = true; this.animationInfo = { textStop: false, bgStop: false }; this.startTime = Date.now(); this.textSprite.update(); this.bgSprite.update(); window.requestAnimationFrame(this.tick.bind(this)); }
tick () { if (!this.animating) { return; } var t = Date.now() - this.startTime; !this.animationInfo.textStop && (this.animationInfo.textStop = this.textSprite.advance(t)); !this.animationInfo.bgStop && (this.animationInfo.bgStop = this.bgSprite.advance(t)); if (this.animationInfo.textStop && this.animationInfo.bgStop) { this.stopPlay(); } else { this.animationInfo.bgStop ? this.bgSprite.drawStatic() : this.bgSprite.drawFrame(); this.animationInfo.textStop ? this.textSprite.drawStatic() : this.textSprite.drawFrame(); window.requestAnimationFrame(this.tick.bind(this)); } }程序架構
程序的整體架構如上圖所示,在入口main.js中,直接新建Editor類實例,并初始化UI組件。
項目中最核心的部分就是Editor類。
Editor包含的數據:
data對象,用于存儲文本數據
selection對象,用于存儲選擇信息
style對象,用于存儲當前樣式信息
map矩陣,用于存儲當前文本對應位置
Editor包含的渲染精靈
bgSprite, 當前渲染背景的精靈
textSprite, 當前渲染文字的精靈
Editor包含的節(jié)點元素:
$input, 隱藏輸入框
$canvas, 用于渲染普通canvas文本
$glcanvas, 用于渲染WebGL文本
$bgCanvas, 用于渲染普通背景
$bgGlcanvas, 用于渲染WebGL背景
這里需要解釋一下為何將文本、背景進行解耦分層。
首先, 每個canvas一旦調用getContext("2d")方法,再調用getContext("WebGL")方法則會返回null。也就是說,同一個canvas只能獲取普通2d context和WebGL context中的一個,這意味著我們無法同時調用WebGL api和原生canvas api。所以對于文字或背景的渲染,都分成WebGL和原生canvas兩種。
另外,由于項目中文本、背景樣式都可以自由切換,若都用同一個canvas進行渲染,保持文本樣式不變,而對背景樣式進行切換時,則整個canvas都要重繪。為避免這樣的開銷,項目中將文本、背景進行分層繪制。
此處或許有人會考慮到最終圖像保存的問題。是的,進行分層后,圖像保存需要另外做一些處理,但并不太復雜,只需將每層canvas圖像逐層繪制到一個離屏canvas上即可。
例如,導出png格式圖片代碼如下:
generatePng () { var canvas = document.createElement("canvas"); canvas.width = this.canvasNode.width; canvas.height = this.canvasNode.height; var ctx = canvas.getContext("2d"); ctx.drawImage(this.bgCanvasNode, 0, 0); ctx.drawImage(this.bgGlcanvasNode, 0, 0); ctx.drawImage(this.canvasNode, 0, 0); ctx.drawImage(this.glcanvasNode, 0, 0); var imgData = canvas.toDataURL("image/png"); return imgData; }
下圖描述了項目核心結構、流程:
其中,樣式切換是一個關鍵流程。項目中將樣式配置統(tǒng)一保存在config.js文件中。
其中樣式索引保存在config.state對象中:
state: { fontIndex: 0, fontSizeIndex: 0, fontColorIndex: 0, textStyleIndex: 0, textAlignIndex: 0, backgroundIndex: 0, animationIndex: 1, bgColorIndex: 0 }
而對應可切換的樣式定義保存在相應map數組中。舉個例子,對背景樣式的配置如下:
backgroundMap: [ { Klass: "PureBgSprite", label: "純色", value: 0, colors: ["rgb(235, 235, 235)", "#FEFEFE", "#3a3a3a"] }, { Klass: "TreeBgSprite", label: "月下林間", value: 1, colors: ["rgb(235, 235, 235)", "#b1a69b", "#3a3a3a"] } ]
backgroundMap數組中每項對應一個樣式選擇,Klass描述了定義該樣式的精靈類名,label定義了工具欄中顯示的樣式名稱,value即對應的樣式索引,colors定義了該背景支持的切換顏色。
每次切換背景樣式時,程序會根據Klass獲取相應精靈實例,并將editor對象中的bgSprite指向該精靈實例。這里特別注意一下,為保證每個精靈對象從始至終都只有一個實例,這里應用了單例模式。
根據類名獲取對象實例的方法定義如下:
getSpriteEntity: function () { var entities = []; return function (className, editor) { var Klass = eval(className); return entities[className] ? entities[className] : entities[className] = new Klass(editor); }; }()
每次樣式切換時,會把map中定義的具體參數賦給style對象,渲染時根據樣式參數進行不同處理。
后續(xù)到此為止,本文主要描述了編輯器的架構以及實現(xiàn)。而其中一些有趣的細節(jié)實現(xiàn)(如WebGL文本渲染,對中文筆畫分割實現(xiàn)有趣的動畫等)并沒有描寫。這些將來會單開博文來寫。
同時項目還有許多常用功能沒有實現(xiàn),比如光標位置切換不支持上下鍵,無法選擇文本等,這些留作以后完善吧。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82194.html
摘要:將所有關聯(lián)對合并即并查集的過程,得到每個連通域的唯一標記。此時每個連通域輪廓可以看做是一個多邊形,此時可以用經典算法將其剖分成若干個三角形。若值為則說明該像素處于當前連通域中。二維數組,表示每個像素是否是圖像邊緣。 筆者另一篇文章 https://segmentfault.com/a/11... 講了基于Canvas的文本編輯器簡詩的實現(xiàn),其中文字由WebGL渲染藝術效果,這篇文章主要...
摘要:在文末,我會附上一個可加載的模型方便學習中文藝術字渲染用原生可以很容易地繪制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以說是 HTML5 技術生態(tài)鏈中最為令人振奮的標準之一,它把 Web 帶入了 3D 的時代。 初識 WebGL 先通過幾個使用 Web...
摘要:但需要注意的是,需在使用前調用。當然,你愿意的話也可以兩者結合著用。繪制圖像相信很多入門的,都看不到這個地方,不就是繪制圖像的嘛,啊不準確,是繪制圖形的。明確的說,是指圍繞原點圖像旋轉弧度。 前言 本文寫在七月底,進來不加班就整理了一下,一些非常基礎的知識,對于canvas剛入門的人來說,值得閱讀一下。 來個氣勢如虹的開頭 與看各種文章相比,我更喜歡數學里的邏輯;與學習各種日新月異的框...
摘要:最終方案也確定采用序列幀動畫方案。所以,要想在電影或者視頻上顯示效果,首先要做的是編寫特效文件,然后再將特效文件解析成序列幀動畫的位圖,最后將這些位圖按照特定的順序和一定的幀率進行播放,就能看到各種特效的動畫。 本文由云+社區(qū)發(fā)表作者:QQ音樂技術團隊 一、 背景 1. 現(xiàn)狀 歌詞瀏覽已經成為音樂app的標配,展示和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的...
摘要:最終方案也確定采用序列幀動畫方案。所以,要想在電影或者視頻上顯示效果,首先要做的是編寫特效文件,然后再將特效文件解析成序列幀動畫的位圖,最后將這些位圖按照特定的順序和一定的幀率進行播放,就能看到各種特效的動畫。 本文由云+社區(qū)發(fā)表作者:QQ音樂技術團隊 一、 背景 1. 現(xiàn)狀 歌詞瀏覽已經成為音樂app的標配,展示和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的...
閱讀 2417·2021-11-19 09:40
閱讀 3585·2021-10-12 10:12
閱讀 1892·2021-09-22 15:04
閱讀 2905·2021-09-02 09:53
閱讀 768·2019-08-29 11:03
閱讀 1129·2019-08-28 18:11
閱讀 1730·2019-08-23 15:28
閱讀 3583·2019-08-23 15:05