成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

用Canvas實(shí)現(xiàn)文本編輯器(支持藝術(shù)字渲染與動畫)

OldPanda / 2474人閱讀

摘要:項(xiàng)目中文字由進(jìn)行渲染。待觸發(fā)時(shí),取消中文輸入標(biāo)記,將文字渲染到上。而其中一些有趣的細(xì)節(jié)實(shí)現(xiàn)如文本渲染,對中文筆畫分割實(shí)現(xiàn)有趣的動畫等并沒有描寫。

導(dǎo)言

目前富文本編輯器的實(shí)現(xiàn)主要有兩種技術(shù)方案:一個是利用contenteditable屬性直接對html元素進(jìn)行編輯,如draft.js;另一種是代理textarea + 自定義div + 模擬光標(biāo)實(shí)現(xiàn)。對于類似"word"的經(jīng)典富文本編輯器,一般會采用以上兩種技術(shù)方案之一,而不會考慮用canvas實(shí)現(xiàn)。

事實(shí)上,官方最佳實(shí)踐中已經(jīng)特別聲明了不推薦用canvas實(shí)現(xiàn)編輯器,詳見https://www.w3.org/TR/2dconte...
不推薦的原因包括光標(biāo)位置維護(hù)、鍵盤移動的實(shí)現(xiàn)、以及沒有原生文本輸入處理等等。

既然如此,為何還要用canvas制作文本編輯器呢?這是因?yàn)閷σ恍┨厥獾膭?chuàng)作來說,canvas能更好的實(shí)現(xiàn)展示需求。比如藝術(shù)字效果的渲染,以及文本、背景動畫等。

基于這點(diǎn)想法,便有了“簡詩”這個自娛自樂的小項(xiàng)目。

簡詩是為短詩文創(chuàng)作而開發(fā)的文本編輯器,主要面向中文寫作。中文最特別之處便在于其筆畫,所以在開發(fā)之初,我便想對文字進(jìn)行處理之時(shí),一定要把漢字進(jìn)行筆畫分割,以便實(shí)現(xiàn)更多有趣的效果的。

項(xiàng)目中文字由WebGL進(jìn)行渲染?;舅悸肥窍雀鶕?jù)用戶選擇的字體,將文字寫在離屏canvas上,然后利用getImageData api獲取文字像素?cái)?shù)據(jù),進(jìn)行連通域查詢、分割、邊緣查找及三角化后,由WebGL進(jìn)行渲染。

(注:這種處理方式的好處是對任意系統(tǒng)支持的字體都可以實(shí)現(xiàn)藝術(shù)效果,而無需額外的字體開發(fā)。目前項(xiàng)目中沒有引入字體文件,用到的字體都是Mac內(nèi)置的字體,Mac用戶如發(fā)現(xiàn)其中有的字體系統(tǒng)沒有默認(rèn)安裝,只需到“字體冊”中安裝一下即可)

這一系列過程會單開一篇文章來寫,本文主要描述canvas編輯器核心的實(shí)現(xiàn)。

實(shí)現(xiàn)效果

預(yù)覽地址:https://moyuer1992.github.io/...
源碼地址:https://github.com/moyuer1992...

技術(shù)關(guān)鍵點(diǎn) 文字鍵入(代理輸入框)

用canvas實(shí)現(xiàn)編輯器最關(guān)鍵的一點(diǎn)就是如何監(jiān)聽鍵盤文字輸入,如果通過鍵盤事件自己處理,英文尚可,中文肯定是不可行的。所以還是需要使用原生textarea做一層代理。

代理textarea輸入框是不可見的。這里需特別注意下,若用display: none隱藏輸入框,則無法觸發(fā)focus事件,所以輸入框需要利用z-index來做隱藏。

當(dāng)用戶點(diǎn)擊canvas時(shí),程序控制觸發(fā)textarea的focus事件,繼而用戶輸入時(shí),也自然觸發(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事件時(shí),將輸入框value取出,渲染到canvas中對應(yīng)位置

清空輸入框,繼續(xù)監(jiān)聽

然而,當(dāng)輸入中文時(shí),一些輸入法會出現(xiàn)這種現(xiàn)象:

顯然,當(dāng)使用中文輸入法鍵入拼音時(shí),拼音字母已經(jīng)寫入輸入框中,觸發(fā)了input事件,但事實(shí)上用戶并沒有鍵入完畢。這就導(dǎo)致了最終拼音字母和漢字全部被寫到了canvas上,這并非我們想要的結(jié)果。

如何解決呢?這里需要用到input元素的onCompStart和onCompEnd事件。

當(dāng)中文輸入開始時(shí),會觸發(fā)onCompStart事件,此時(shí)做一個標(biāo)記,告知程序用戶正在中文輸入,input事件觸發(fā)時(shí),判斷當(dāng)前是否正在鍵入中文,若是,則不作任何操作。待onCompEnd觸發(fā)時(shí),取消中文輸入標(biāo)記,將文字渲染到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();
}
虛擬光標(biāo)

用canvas實(shí)現(xiàn)編輯器需要模擬光標(biāo),這里用一個div來實(shí)現(xiàn),設(shè)置position為absolute,用top、left來定位光標(biāo)位置。

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動畫實(shí)現(xiàn)光標(biāo)1s閃動一次。

@keyframes cursor {
  from {
    opacity: 0;
  }

  50% {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

.cursor {
  animation: cursor 1s ease infinite;
}

原理雖然簡單,但是隨著文字、排版、用戶操作變更,如何維護(hù)光標(biāo)位置,是一件較為繁瑣的事。

這里定義了Selection類以存儲用戶選擇區(qū)域。未選擇任何文本的情況下,selection位置及為光標(biāo)所在位置。(目前此項(xiàng)目尚未支持選擇文本功能,但Selection類的設(shè)計(jì)方式對以后此功能的添加是支持的。)

selection對象中,位置存儲完全是針對文本矩陣的,而非對應(yīng)屏幕上真正的坐標(biāo)。項(xiàng)目中另外定義了map矩陣存儲文本位置數(shù)據(jù)。map的具體設(shè)計(jì)下面一節(jié)會詳細(xì)講到。

更新光標(biāo)函數(shù)如下:

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é)中已經(jīng)提到,項(xiàng)目中定義了map矩陣存儲文本位置信息。每次渲染文字時(shí),會依據(jù)當(dāng)前樣式(版式、文字大小等)更新map數(shù)據(jù)。
目前項(xiàng)目支持居中和左對齊兩個版式,map更新時(shí),這兩個版式的位置計(jì)算有所不同。

對于左對齊版式,邏輯比較簡單,只要從左邊邊距處開始,逐個寫入文字,直至換行即可。
而對于居中版式,邏輯要稍微復(fù)雜一些,處理每段文字時(shí),要先根據(jù)每段文字總長度、canvas寬度、邊距大小來確定文字位置。如果此段文字不足一行,則直接居中顯示,若超過一行,將每行填滿后,對不足一行的部分居中顯示。

每個map元素結(jié)構(gòu)如下:

{
  char: 對應(yīng)字符/文字,
  x: 文字起始x坐標(biāo),
  y: 文字起始y坐標(biāo),
  cursorX: 對應(yīng)光標(biāo)x坐標(biāo),
  cursorY: 對應(yīng)光標(biāo)y坐標(biāo)
}
動畫精靈

之所以用canvas實(shí)現(xiàn)文本編輯器,便是為了藝術(shù)效果的渲染以及文字、背景動畫。項(xiàng)目希望實(shí)現(xiàn)文字、背景樣式的自由切換,為了降低耦合度,為每種文字、背景樣式多帶帶定義精靈。

文本精靈基類:https://github.com/moyuer1992...
文本精靈文件夾:https://github.com/moyuer1992...
背景精靈基類:https://github.com/moyuer1992...
背景精靈文件夾:https://github.com/moyuer1992...

精靈類中的核心是drawStatic、drawFrame、advance三個方法。
advance函數(shù)中,對進(jìn)入下一幀時(shí)需要改變的參數(shù)進(jìn)行定義。

drawStatic用于靜態(tài)效果的渲染。Editor類中,每次需要重新渲染靜態(tài)文字時(shí),都會調(diào)用此方法。

_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用于動畫效果每一幀的渲染,當(dāng)動畫播放時(shí),會逐幀調(diào)用此方法。

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));
  }
}
程序架構(gòu)

程序的整體架構(gòu)如上圖所示,在入口main.js中,直接新建Editor類實(shí)例,并初始化UI組件。

項(xiàng)目中最核心的部分就是Editor類。

Editor包含的數(shù)據(jù):

data對象,用于存儲文本數(shù)據(jù)

selection對象,用于存儲選擇信息

style對象,用于存儲當(dāng)前樣式信息

map矩陣,用于存儲當(dāng)前文本對應(yīng)位置

Editor包含的渲染精靈

bgSprite, 當(dāng)前渲染背景的精靈

textSprite, 當(dāng)前渲染文字的精靈

Editor包含的節(jié)點(diǎn)元素:

$input, 隱藏輸入框

$canvas, 用于渲染普通canvas文本

$glcanvas, 用于渲染W(wǎng)ebGL文本

$bgCanvas, 用于渲染普通背景

$bgGlcanvas, 用于渲染W(wǎng)ebGL背景

這里需要解釋一下為何將文本、背景進(jìn)行解耦分層。

首先, 每個canvas一旦調(diào)用getContext("2d")方法,再調(diào)用getContext("WebGL")方法則會返回null。也就是說,同一個canvas只能獲取普通2d context和WebGL context中的一個,這意味著我們無法同時(shí)調(diào)用WebGL api和原生canvas api。所以對于文字或背景的渲染,都分成WebGL和原生canvas兩種。

另外,由于項(xiàng)目中文本、背景樣式都可以自由切換,若都用同一個canvas進(jìn)行渲染,保持文本樣式不變,而對背景樣式進(jìn)行切換時(shí),則整個canvas都要重繪。為避免這樣的開銷,項(xiàng)目中將文本、背景進(jìn)行分層繪制。

此處或許有人會考慮到最終圖像保存的問題。是的,進(jìn)行分層后,圖像保存需要另外做一些處理,但并不太復(fù)雜,只需將每層canvas圖像逐層繪制到一個離屏canvas上即可。

例如,導(dǎo)出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;
}

下圖描述了項(xiàng)目核心結(jié)構(gòu)、流程:

其中,樣式切換是一個關(guān)鍵流程。項(xiàng)目中將樣式配置統(tǒng)一保存在config.js文件中。
其中樣式索引保存在config.state對象中:

state: {
  fontIndex: 0,
  fontSizeIndex: 0,
  fontColorIndex: 0,
  textStyleIndex: 0,
  textAlignIndex: 0,
  backgroundIndex: 0,
  animationIndex: 1,
  bgColorIndex: 0
}

而對應(yīng)可切換的樣式定義保存在相應(yīng)map數(shù)組中。舉個例子,對背景樣式的配置如下:

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數(shù)組中每項(xiàng)對應(yīng)一個樣式選擇,Klass描述了定義該樣式的精靈類名,label定義了工具欄中顯示的樣式名稱,value即對應(yīng)的樣式索引,colors定義了該背景支持的切換顏色。

每次切換背景樣式時(shí),程序會根據(jù)Klass獲取相應(yīng)精靈實(shí)例,并將editor對象中的bgSprite指向該精靈實(shí)例。這里特別注意一下,為保證每個精靈對象從始至終都只有一個實(shí)例,這里應(yīng)用了單例模式。

根據(jù)類名獲取對象實(shí)例的方法定義如下:

getSpriteEntity: function () {
  var entities = [];
  return function (className, editor) {
    var Klass = eval(className);
    return entities[className] ? entities[className] : entities[className] = new Klass(editor);
  };
}()

每次樣式切換時(shí),會把map中定義的具體參數(shù)賦給style對象,渲染時(shí)根據(jù)樣式參數(shù)進(jìn)行不同處理。

后續(xù)

到此為止,本文主要描述了編輯器的架構(gòu)以及實(shí)現(xiàn)。而其中一些有趣的細(xì)節(jié)實(shí)現(xiàn)(如WebGL文本渲染,對中文筆畫分割實(shí)現(xiàn)有趣的動畫等)并沒有描寫。這些將來會單開博文來寫。

同時(shí)項(xiàng)目還有許多常用功能沒有實(shí)現(xiàn),比如光標(biāo)位置切換不支持上下鍵,無法選擇文本等,這些留作以后完善吧。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/82194.html

相關(guān)文章

  • Canvas + WebGL中文藝術(shù)渲染

    摘要:將所有關(guān)聯(lián)對合并即并查集的過程,得到每個連通域的唯一標(biāo)記。此時(shí)每個連通域輪廓可以看做是一個多邊形,此時(shí)可以用經(jīng)典算法將其剖分成若干個三角形。若值為則說明該像素處于當(dāng)前連通域中。二維數(shù)組,表示每個像素是否是圖像邊緣。 筆者另一篇文章 https://segmentfault.com/a/11... 講了基于Canvas的文本編輯器簡詩的實(shí)現(xiàn),其中文字由WebGL渲染藝術(shù)效果,這篇文章主要...

    baihe 評論0 收藏0
  • SegmentFault 技術(shù)周刊 Vol.35 - WebGL:打開網(wǎng)頁看大片

    摘要:在文末,我會附上一個可加載的模型方便學(xué)習(xí)中文藝術(shù)字渲染用原生可以很容易地繪制文字,但是原生提供的文字效果美化功能十分有限。 showImg(https://segmentfault.com/img/bVWYnb?w=900&h=385); WebGL 可以說是 HTML5 技術(shù)生態(tài)鏈中最為令人振奮的標(biāo)準(zhǔn)之一,它把 Web 帶入了 3D 的時(shí)代。 初識 WebGL 先通過幾個使用 Web...

    objc94 評論0 收藏0
  • canvas入門里,你沒注意到的那些知識

    摘要:但需要注意的是,需在使用前調(diào)用。當(dāng)然,你愿意的話也可以兩者結(jié)合著用。繪制圖像相信很多入門的,都看不到這個地方,不就是繪制圖像的嘛,啊不準(zhǔn)確,是繪制圖形的。明確的說,是指圍繞原點(diǎn)圖像旋轉(zhuǎn)弧度。 前言 本文寫在七月底,進(jìn)來不加班就整理了一下,一些非?;A(chǔ)的知識,對于canvas剛?cè)腴T的人來說,值得閱讀一下。 來個氣勢如虹的開頭 與看各種文章相比,我更喜歡數(shù)學(xué)里的邏輯;與學(xué)習(xí)各種日新月異的框...

    tuniutech 評論0 收藏0
  • QQ音樂的動效歌詞是如何實(shí)踐的?

    摘要:最終方案也確定采用序列幀動畫方案。所以,要想在電影或者視頻上顯示效果,首先要做的是編寫特效文件,然后再將特效文件解析成序列幀動畫的位圖,最后將這些位圖按照特定的順序和一定的幀率進(jìn)行播放,就能看到各種特效的動畫。 本文由云+社區(qū)發(fā)表作者:QQ音樂技術(shù)團(tuán)隊(duì) 一、 背景 1. 現(xiàn)狀 歌詞瀏覽已經(jīng)成為音樂app的標(biāo)配,展示和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的...

    Edison 評論0 收藏0
  • QQ音樂的動效歌詞是如何實(shí)踐的?

    摘要:最終方案也確定采用序列幀動畫方案。所以,要想在電影或者視頻上顯示效果,首先要做的是編寫特效文件,然后再將特效文件解析成序列幀動畫的位圖,最后將這些位圖按照特定的順序和一定的幀率進(jìn)行播放,就能看到各種特效的動畫。 本文由云+社區(qū)發(fā)表作者:QQ音樂技術(shù)團(tuán)隊(duì) 一、 背景 1. 現(xiàn)狀 歌詞瀏覽已經(jīng)成為音樂app的標(biāo)配,展示和動畫效果也基本上大同小異,主要是單行的逐字染色的卡拉OK效果和多行的...

    Scholer 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<