精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

圖形編輯器:基于 Canvas 的所見即所得文本編輯

開發 前端
文本編輯,可以看作是對一個個矩形塊進行編排,我們計算好每個字形 glyph 的包圍盒,編排成一行或多行的文字。

大家好,我是前端西瓜哥。

前段時間給我的 suika 圖形編輯器重寫了文本編輯功能,基本支持了所見即所地編輯文本了,這篇文章總結一下實現這個功能需要做的一些工作。

suika 圖形編輯器 github 地址:

https://github.com/F-star/suika

線上體驗:

https://blog.fstars.wang/app/suika/

簡單演示,使用的字體是 “得意黑”。

作為一款圖形編輯器,自然是少不了文本的輸入和編輯功能。

為了提高性能,圖形編輯器通常使用 canvas 實現,但文本編輯如果要用 canvas 實現是不小的工作量。

對此,一種簡單的方式是進入編輯狀態時隱藏原來的文本圖形,然后在其正上方通過絕對定位放上一個 input 或 texture,如果還想支持富文本,也可以找一個富文本編輯器掛載在 div 容器元素上,再把這個 div 做絕對定位。

這種借助 html 元素的方式,在簡單場景倒是沒什么問題。

但它有如下缺陷:

  • 無法保持文本圖形原來所在的層級,只能提升到頂層。這樣就不能所見即所得的看文本圖形和它上方圖形效果疊加的效果,比如沒法實時觀察并調整毛玻璃濾鏡下文字渲染效果。
  • canvas 和 html 渲染效果不一致。圖形編輯器的文本渲染可能做了一些加強,比如右上小標、文字使用漸變填充、圖片填充、虛線描邊等各種增強功能。這是基于 html 的文本編輯器是無法模擬的,且不同瀏覽器的 html 渲染也有微妙不同。

出于精益求精的精神,我們嘗試在圖形編輯器下,做一個基于 canvas 2d 的簡單文本編輯器。

文本圖形

文本圖形實體。

class TextGraphis {
  attrs: {
    content: string
  }
}

這里我們就不這么復雜,用純文本,提供一個 content 屬性,保存字符串形式的文本內容。

字形 box

然后我們需要計算 content 中每個字形(glyph)的寬高,之后需要用它們來定位文字游標的位置。

interface IGlyph {
  position: IPoint;
  width: number;
  height: number;
  // box 頂部到基線的距離
  fontBoundingBoxAscent: number;
}

注意這里說的不是每個字符(char),這是因為數據上的多個字符的表達,在渲染時可能會合并為一個。

JavaScript 支持 Unicode,一個 Unicode 字符可能會占用 2 個或更多碼點 的空間,比如 "??"。

"??".length 的返回值是 2,雖然看起來只有一個字符。?? 其實等價于 \uD842\uDFB7。

一個 Unicode 可以簡單和一個 glyph 劃等號(暫不考慮連字 ligature)。

emoji 也是 Unicode,對于 canvas 2d,如果字體的字符集中有對應的 emoji,會將這個 emoji 渲染出來,否則用操作系統提供的 emoji 進行渲染。

我們沒法用字符串的 length 屬性來判斷 glyph 的數量。

我們可以用 for...of 來拿到每個 Unicode 字符,然后用 ctx.measureText() 方法拿到每個 glyph 的 box 信息。

const glyphs = [];

for (const c of content) {
  const textMetrics = ctx.measureText(c);
  glyphs.push({
    position: { ...position },
    width: textMetrics.width,
    height:
      textMetrics.fontBoundingBoxAscent + textMetrics.fontBoundingBoxDescent,
    fontBoundingBoxAscent: textMetrics.fontBoundingBoxAscent,
  });
  position.x += textMetrics.width;
}

fontBoundingBoxAscent 為 box 頂部距離文本基線(baseline)的距離,fontBoundingBoxDescent 為 box 底部距離文本基線的距離,二者相加即為 box 的高度。

fontBoundingBoxAscent 屬性我們也保存下來,canvas 2d 渲染是基于基線的,我們需要這個值做垂直位移。

position 記錄了 glyph 的左上角到文本起點位置的距離。

比較遺憾的是,canvas 2d 拿不到字體的 kerning 字距表。

我也有想到一個辦法,比較曲折,就是分別單獨計算兩個字符的各自的寬度,然后再計算兩個字符拼接后的寬度。

求出這兩個寬度的差值,便是這兩個字符的字距了。

這里可以優化一下,相同的 glyph 沒有必要重新計算,可以用一個 map 緩存起來。

Range

我們拿到了字符串中每個 glyph 的幾何信息,就能正確的位置渲染 cursor 光標了。

首先我們定義一個 RangeManager 類,來 維護文本中的光標線和選中信息。

class RangeManager {
  private range = { start: 0, end: 0 };
  
  setRange(range) {
    this.range = {
      start: range.start,
      end: range.end,
    };
  }

  getRange() {
    return { ...this.range };
  }
}

成員屬性 range 的 start 表示被 編輯文本上選區的起始索引值,end 表示選區的結束位置。

當 start 和 end 值相等時,在最上層會顯示一個閃爍的豎直光標,位置為對應 glyph 的左側。

閃爍動畫可能會導致渲染不斷被觸發,需要做一些優化,目前 suika 圖形編輯器上的文字光標目前并不會閃爍。

另外我們還有一個方案,就是像 canvas editor 一樣,用一個帶動畫的 div 模擬,反正它都是要放在最頂部的。

如果 start 和 end 的值不同,則是將這個區間內繪制一個半透明的矩形,同樣是放到最頂層。

start 的值并不要求一定小于 end ,是可以大于 end 的。后面我們用光標選中字符時需要用到這個特性。

但有時候我們希望拿到基于左右位置拿到兩個索引值,用于正確切割出 range 左右兩側的子字符串。

所以我們要加個 getSortedRange 方法。

class RangeManager {
  // ...

  getSortedRange() {
    const rangeLeft = Math.min(this.range.start, this.range.end);
    const rangeRight = Math.max(this.range.start, this.range.end);
    return { rangeLeft, rangeRight };
  }
}

光標位置計算

如果 range.start 和 range.end 相等,我們會渲染一條光標線,為此我們需要計算這條線的 top 和 bottom 位置,見下圖。

做法是拿到正在被編輯的文本圖形實體的字形信息,即前面提到的字形 box 數組。

const glyphInfos = textGraphics.getGlyphs();

根據 range.start 的索引值找到匹配的 glyph 項,對應的 position 是相對文本實體的本地坐標,我們需要應用文本的矩陣得到場景坐標。

又因為我們需要把光標渲染在最頂層,也就是視口坐標系上,所以我們又要再做一個場景坐標到視口坐標的轉換。自此 top 計算出來了。

const startGlyphInfo = glyphInfos[range.start]
// 文本實體上的光標位置
const cursorPosInText = startGlyphInfo.position;
const textMatrix = textGraphics.getWorldTransform();
// 場景坐標
const top = applyMatrix(textMatrix, cursorPosInText);
// 畫布坐標
const topInViewport = this.editor.toViewportPt(top.x, top.y);

bottom 位置同理,加上高度再進行同樣的矩陣變換。

const bottom = applyMatrix(textMatrix, {
  x: cursorPosInText.x,
  y: cursorPosInText.y + contentHeight,
});
const bottomInViewport = this.editor.toViewportPt(bottom.x, bottom.y);

如果 range.start 和 range.end 不相等,則渲染為一個半透明的矩形,當然因為矩陣變換的緣故,也可能會變成一個平行四邊形。

我們要計算這個平行四邊形的 4 個點,前面我們已經算出 top 和 bottom 這兩個點了,我們再計算一個 right,見下圖。

計算過程也大同小異,right 對應 range.end 索引位置的 glyph。

let rightInViewport = null;

if (range.end !== range.start) {
  const endGlyphInfo = glyphInfos[range.end]
  const endPosInText = endGlyphInfo.position;
  const right = applyMatrix(textMatrix, endPosInText);
  rightInViewport = this.editor.toViewportPt(right.x, right.y);
}

top、bottom、right 這三個點,再基于平行四邊形(矩形做了矩陣變換)的特征,可以算出最后一個點,然后就可以進行渲染了。

具體怎么渲染就不展開了,不同渲染庫寫法不一樣。

輸入法定位問題

下面我們看看,怎么通過鍵盤輸入文本。

既然都做所見即所得了,看起來我們不需要用 input、textarea 這些 dom 元素了,直接監聽 keydown 事件應該就好了。

但實際上它是有局限性的,它只能用在不需要輸入法的場景,比如只輸入英文。如果你用輸入法輸入中文,因為沒有 focus 一個輸入框中,所以不會有輸入法的浮窗出現。

所以我們還是要 提供一個文本輸入元素并讓它保持 focus 狀態。

這里我選擇用 input 元素,因為我的文本編輯首先還是比較簡單的。

input 元素雖然必須要在,但讓它看起來不在就行了。

我們把它的不透明度設置為 0,然后 z-index 設置為 -1,寬度也改成 1px(保證 input 下的光標保持在 input 框中的起始位置)。

const defaultInputStyle = {
  opacity: 0,
  zIndex: '-1',
  width: '1px',

  margin: 0,
  padding: 0,
  border: 0,
  outline: 0,

  position: 'fixed',
}

fixed 定位

為了讓輸入法彈窗定位到正確的位置,我們需要 給 input 設置 fixed 定位。

我們確保 input 的左下角對齊前面計算的那個 bottomInViewport 即可。

具體計算為:left 為前面計算的那個 bottomInViewport 的 x,再加上 canvas 相對頁面左測的偏移值;top 為 bottomInViewport 的 y 值減去文本的字體大小,再加上 canvas 相對頁面頂部的偏移值。

const styles = {
  left: bottomInViewport.x + canvasOffsetX + 'px',
  top: bottomInViewport.y - inputDomHeight + canvasOffsetY + 'px',
  height: `${inputDomHeight}px`,
  fontSize: `${inputDomHeight}px`,
}
Object.assign(inputDom.style, styles);

這時候有的同學可能會問了,問我怎么不用 absolute 定位,相對 canvas 的容器元素。說實話我是用過的,然后發現一個 input 元素的特性。

就是如果一個 input 元素在一個 div 下,但是呢,它跑到 div 的顯示區域外,看不到它。

當這個 input 是 focus 狀態時,那瀏覽器會強行修改 div 的 offset 讓 input 可以被看到,結果是突然 div 上出現了一大塊空白區域,主體內容被擠不見了。

換成 fixed 就不會有這個問題,輸入法彈窗會移動頁面外,但不會影響頁面的布局。

輸入文本

當文本編輯被激活時,這個 input 會設置為 focus 狀態。

此時我們監聽 input 元素的 input 事件,將用戶輸入的內容更新到文本實體 textGraphics 上,并修正 range。

我們可以通過 input 事件對象的 isComposing 是否為 true 判斷用戶是否在使用輸入法。

簡單輸入

首先是比較簡單的場景,不輸入中文的情況。

inputDom.addEventListener('input', (e) => {
    
  // ...
    
  // Not IME input, directly add to textGraphics
  if (!e.isComposing && e.data) {
    const { rangeLeft, rangeRight } = rangeManager.getSortedRange();

    const content = textGraphics.attrs.content;
    const newContent =
      sliceContent(content, 0, rangeLeft) +
      e.data +
      sliceContent(content, rangeRight);

    // 更新文本實體的 content 和 size
    TextEditor.updateTextContentAndResize(textGraphics, newContent);
    const dataLength = getContentLength(e.data);
    // 更新 range 的狀態,往右邊移動 e.date 的長度
    this.rangeManager.setRange({
      start: rangeLeft + dataLength,
      end: rangeLeft + dataLength,
    });
  }
}

e.isComposing 為 false 表示沒有在使用輸入法,然后 e.data 保存的是用戶輸入的內容。

需要注意,e.data 可能存在為 null 的情況,比如 backspace 刪除字符,粘貼空內容,這種情況需要過濾掉。

我們在 content 字符串 range 區域的字符串丟棄,然后將 e.data 的字符串拼接進去,得到 newContent,并對文本實體 textGraphics 進行更新,最后更新 range 的狀態,往右邊移動 e.date 的長度。

因為 unicode 的存在,我們不能用字符串的 length 屬性了,那都是騙人的,要改用 for...of 去實現一些字符串方法。

// 獲取字符串的長度
const getContentLength = (content) => {
  let count = 0;
  for (const _ of content) {
    count++;
  }
  return count;
};

// 字符串截斷
const sliceContent = (content, start, end) => {
  let res = '';
  let i = 0;
  for (const char of content) {
    if (end !== undefined && i >= end) {
      break;
    }
    if (i >= start) {
      res += char;
    }
    i++;
  }
  return res;
};

通過輸入法輸入

如果使用了輸入法,情況會復雜一點。

這種場景下,e.isComposing 為 true,e.data 則是用戶正在輸入的內容。

比如我想輸入 “你好”,通過拼音輸入法進行完整的拼音輸入,最后按下空格。這個過程中 input 事件會多次觸發,e.data 依次為:

n
ni
ni h
ni ha
ni hao
你好

所以我們不能將每次 input 事件的 e.data 直接拼接到 content 上。

我們需要在  e.isComposing 第一次為 true 時,保存好 range 兩邊的字符串內容,以及 e.data 的內容。

之后就將開始時兩邊的字符串和  e.data 拼接即可。

inputDom.addEventListener('input', (e) => {
  let composingText = '';
  let leftContentWhenComposing = '';
  let rightContentWhenComposing = '';
    
  if (e.isComposing) {
    if (!composingText) {
      // 輸入法第一次輸入內容,保存好 range 兩邊的內容
      const { rangeLeft, rangeRight } = rangeManager.getSortedRange();
      const content = textGraphics.attrs.content;
      leftContentWhenComposing = sliceContent(content, 0, rangeLeft);
      rightContentWhenComposing = sliceContent(content, rangeRight);
    }
    composingText = e.data ?? '';
  } else {
    // 重置
    composingText = '';
    leftContentWhenComposing = '';
    rightContentWhenComposing = '';
  }
  
  // ...
  
  if (e.isComposing) {
    const newContent =
      leftContentWhenComposing + composingText + rightContentWhenComposing;
    
    TextEditor.updateTextContentAndResize(textGraphics, newContent);
   // 更新 range
    const newRangeStart =
      getContentLength(leftContentWhenComposing) +
      getContentLength(composingText);
    rangeManager.setRange({
      start: newRangeStart,
      end: newRangeStart,
    });
  }
})

各種快捷鍵行為

然后是監聽 input 的 keydown 事件,實現各種編輯操作。

inputDom.addEventListener('keydown', (e) => {
  // ...
})

Esc,退出文本編輯模式。

if (e.key === 'Escape') {
  this.inactive();
}

左方向鍵,如果光標狀態,range 左移動一位;如果選擇狀態,range 置為 rangeLeft。

如果還按住 Shift 鍵,只對 range.end 減 1。注意 range 的索引值不要越界。

if (e.key === 'ArrowLeft') {
  if (e.shiftKey) {
    this.rangeManager.moveRangeEnd(-1);
  } else {
    this.rangeManager.moveLeft();
  }
}

右方向鍵,同理。

Backspace,如果是光標狀態,往左刪掉一個字符,range 左移一位;如果是選中多個 字符狀態,刪掉這些字符,range 設置為 rangeLeft 。

Delete,類似 Backspace,但是是往右側刪除。

if (e.key === 'Backspace' || e.key === 'Delete') {
  let { rangeLeft, rangeRight } = this.rangeManager.getSortedRange();
  const isSelected = rangeLeft !== rangeRight;

  if (!isSelected) {
    rangeLeft = e.key === 'Backspace' ? rangeLeft - 1 : rangeLeft;
    rangeRight = e.key === 'Backspace' ? rangeRight : rangeRight + 1;
  }

  const content = textGraphics.attrs.content;
  const leftContent = sliceContent(content, 0, rangeLeft);
  const rightContent = sliceContent(content, rangeRight);
  const newContent = leftContent + rightContent;
  TextEditor.updateTextContentAndResize(textGraphics, newContent);

  if (isSelected) {
    rangeManager.setRange({
      start: rangeLeft,
      end: rangeLeft,
    });
  } else if (e.key === 'Backspace') {
    rangeManager.moveLeft();
  }
}

Command / Ctrl + A,全選,將 range 區間設置為 content 的完全的區間。

this.rangeManager.setRange({
  start: 0,
  end: this.textGraphics.getContentLength(),
});

Command / Ctrl + C,復制。將 range 區間的文本寫入到剪貼板

Command / Ctrl + X,剪切。將 range 區間的文本寫入到剪貼板,然后將 range 的內容丟棄。

鼠標選中

下面看看怎么通過鼠標來進行文本的選擇。

我們需要綁定 canvas 元素的鼠標事件,這個我原本就封裝好了,其實也就是 canvas 上的鼠標事件對象拿到視口坐標,通過矩陣轉換成場景坐標。

點擊鼠標時,我們拿到這個場景坐標,然后我們給這個場景坐標做文本實體矩陣的 逆矩陣運算,得到在文本實體的本地坐標。

然后就是 glyph 數組的 x 和鼠標位置的位置,找到被點中的 glyph。

class TextGraphics {
  // ...

  getCursorIndex(point) {
    // 逆矩陣得到本地坐標
    point = applyInverseMatrix(this.attrs.transform, point);
    const glyphs = this.getGlyphs();

    // binary search, find the nearest but not greater than point.x glyph index
    let left = 0;
    let right = glyphs.length - 1;
    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const glyph = glyphs[mid];
      if (point.x < glyph.position.x) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    if (left === 0) return 0;
    if (left >= glyphs.length) return glyphs.length - 1;

    if (
      glyphs[left].position.x - point.x >
      point.x - glyphs[right].position.x
    ) {
      return right;
    }
    return left;
  }
}

這里用了二分查找,效率很高。

找到 glyph 后,我們還要看一下鼠標位置靠近 glyph 的左半部分還是右半部分,設置為更靠近的一邊的索引值。

然后將這個索引值設置為 range 即可。

const cursorIndex = textGraphics.getCursorIndex(mousePt);
this.rangeManager.setRange({
  start: cursorIndex,
  end: cursorIndex,
});

然后此時拖拽鼠標,我們使用同樣的方式,計算出索引值,設置給 range.end。

結尾

文本編輯,可以看作是對一個個矩形塊進行編排,我們計算好每個字形 glyph 的包圍盒,編排成一行或多行的文字。

然后引入 range  的概念,用來表達目前光標在哪里,或哪些矩形塊被選中。

最后再通過監聽鍵盤事件和 mouse 事件更新 range,并通過 input事件獲取用戶輸入內容,直接更新到文本圖形上。

這個文本編輯器還是比較簡單,但基本的核心已經具備,希望對你有幫助。

責任編輯:姜華 來源: 前端西瓜哥
相關推薦

2021-02-27 21:20:31

工具編輯器CKEditor

2022-06-13 08:24:45

Typora編輯器

2010-02-04 11:13:49

WEB編輯器

2023-04-17 11:03:52

富文本編輯器MTE

2013-10-23 11:06:54

HTML5開發框架

2016-09-23 20:30:54

Javascriptuiwebview富文本編輯器

2018-09-07 17:45:19

華為云

2010-03-24 09:20:07

CentOS vi編輯

2020-12-23 22:25:11

Vi文本編輯器Unix

2021-01-07 11:00:59

Sed文本編輯器Linux

2022-05-13 15:32:11

GNOME文本編輯器

2020-12-13 12:14:45

H5開發H5-Dooring

2011-11-16 17:34:57

編輯器

2020-08-20 15:16:27

微軟開源Windows

2009-12-09 10:27:03

VS 2005文本編輯

2013-11-18 10:08:56

工具免費編程工具

2011-05-11 10:27:42

文本編輯器

2012-09-29 11:38:27

編程工具文本編輯器編程
點贊
收藏

51CTO技術棧公眾號

国产免费一区二区三区在线观看| 欧美午夜精品久久久久久久| 91久久国产自产拍夜夜嗨| 青青草激情视频| 久久午夜影院| 在线免费观看一区| 一区中文字幕在线观看| 亚洲免费成人网| 视频在线在亚洲| 欧美另类第一页| 亚洲第一成人网站| 国产aa精品| 一本一本大道香蕉久在线精品 | 国产视频第二页| 亚洲精品1区| 日韩中文字幕视频在线观看| 涩视频在线观看| 999国产精品亚洲77777| 黑人操亚洲女人| 色呦哟—国产精品| 日韩黄色在线免费观看| 久久精品亚洲天堂| 欧美日韩精品免费观看视完整| 一区二区久久久久| 日韩高清dvd| 少妇荡乳情欲办公室456视频| 久久国产精品露脸对白| 欧洲成人性视频| 国产亚洲精品码| 五月天久久网站| 夜夜躁日日躁狠狠久久88av| 国产老熟女伦老熟妇露脸| 欧美经典一区| 8v天堂国产在线一区二区| 国产偷人视频免费| 天天综合av| 亚瑟在线精品视频| 91黄色在线看| 国产秀色在线www免费观看| 久久久久99精品一区| 国产视频一区二区不卡| 亚洲成人77777| 精品一区二区日韩| 国产精品嫩草视频| 久久夜色精品国产噜噜亚洲av| 亚洲激情亚洲| 欧美激情综合亚洲一二区| 91插插插插插插| 羞羞答答成人影院www| 中文字幕综合一区| 免费在线观看a视频| 久久不见久久见免费视频7| 亚洲精品久久久久久下一站| 男男做爰猛烈叫床爽爽小说| 国产亚洲成av人片在线观黄桃| 精品久久久久久无| 亚洲色图欧美另类| jizzjizzjizz欧美| 亚洲国产精品热久久| 中文在线永久免费观看| 日韩手机在线| 亚洲欧洲成视频免费观看| 久久精品国产亚洲av麻豆| 日韩高清一级| 国产亚洲精品va在线观看| 久久午夜福利电影| 成人综合专区| 久久这里有精品视频| 午夜精品福利在线视频| 欧美三级网页| 久久久久亚洲精品国产| 成人免费区一区二区三区| 国产精品试看| 国产精品久久久久久久久粉嫩av| 中文字幕av网站| 国产一区在线看| 国产福利久久| 欧美少妇另类| 国产精品二三区| 免费的av在线| 国产在线看片免费视频在线观看| 欧美香蕉大胸在线视频观看| 国产区二区三区| 免费观看亚洲天堂| 亚洲国产精品99| 日本乱子伦xxxx| 五月开心六月丁香综合色啪| 午夜精品福利在线观看| 久久精品久久久久久久| 精品一区二区三区免费播放 | 亚洲天堂视频在线播放| 极品少妇一区二区| 国产丝袜不卡| 97在线观看免费观看高清| 亚洲精品久久7777| 欧美 国产 日本| а天堂中文最新一区二区三区| 精品成人一区二区三区四区| 欧美黄色一级生活片| 欧美黄免费看| 国产精品va在线播放| 午夜精品久久久久久久99热黄桃 | 久久婷婷国产综合尤物精品| 在线观看免费高清完整| 一区二区三区四区高清精品免费观看| 鲁一鲁一鲁一鲁一澡| 四虎永久精品在线| 精品视频在线播放| 欧美激情图片小说| 日韩精品每日更新| 国产综合第一页| 黄色一级大片在线免费看产| 精品久久久中文| 亚洲男人天堂2021| 精品影片在线观看的网站| 欧美成人精品一区| 黄色一区二区视频| 97se狠狠狠综合亚洲狠狠| 黄色一级片网址| 午夜av成人| 亚洲精品国产福利| 欧美精品videos极品| 毛片av一区二区| 久久综合福利| 波多野结依一区| 91精品在线麻豆| 免费黄在线观看| 中文日韩欧美| 国产精品视频福利| 污污视频在线看| 欧美老年两性高潮| 调教驯服丰满美艳麻麻在线视频| 日韩视频二区| 成人资源av| a天堂中文在线官网在线| 欧美日韩二区三区| 日本黄色小视频在线观看| 三级亚洲高清视频| 欧美18视频| 亚洲人成在线网站| 日韩av有码在线| 色网站在线播放| 成人一道本在线| 日韩免费在线观看av| 视频一区在线| 蜜臀久久99精品久久久无需会员| 97精品人妻一区二区三区| 国产精品久久久久影院色老大| 美女一区二区三区视频| 欧美日韩激情| 国产精品无码专区在线观看| 在线视频婷婷| 欧美日韩国产美| 成人涩涩小片视频日本| 国产原创一区二区| 人妻激情另类乱人伦人妻| 日韩中文字幕视频网| 欧美高跟鞋交xxxxxhd| 亚洲国产999| 亚洲成a人片综合在线| 亚洲一级av无码毛片精品| 9色国产精品| 欧美日韩综合精品| 99久久精品一区二区成人| 色偷偷噜噜噜亚洲男人| 国产三级小视频| 亚洲成av人片一区二区三区| 短视频在线观看| 日本女优在线视频一区二区 | 免费成人黄色| 日韩欧美中文字幕精品| 精品在线视频免费| 91老师片黄在线观看| 欧美综合在线观看视频| 91亚洲成人| av免费观看久久| 男人久久天堂| 一个色综合导航| 国产又大又粗又长| 亚洲成a人片在线观看中文| 精品国产av无码| 国产在线麻豆精品观看| 久草视频国产在线| 成人在线免费观看91| julia一区二区中文久久94| 三妻四妾的电影电视剧在线观看 | 精品乱子伦一区二区三区| 日韩大片欧美大片| 大胆欧美人体视频| 视频一区二区三区在线看免费看 | 欧美又大又粗又长| 毛片在线播放a| 日韩理论片久久| 夜夜嗨av禁果av粉嫩avhd| 亚洲一卡二卡三卡四卡五卡| 受虐m奴xxx在线观看| 国产麻豆精品theporn| 欧美丰满熟妇bbbbbb百度| 97精品中文字幕| 精品无码久久久久国产| 91精品网站在线观看| 欧美亚洲国产精品| 日本高清在线观看视频| 伊人久久综合97精品| 日本黄色三级视频| 欧美日韩另类一区| 国产精品999在线观看| 中文字幕亚洲视频| 波多野结衣办公室33分钟| 国产精品综合久久| 色一情一乱一伦一区二区三区日本| 国产一区激情| 亚洲在线视频一区二区| 亚洲老女人视频免费| 97久久天天综合色天天综合色hd| 精品欧美一区二区三区在线观看| 久久久这里只有精品视频| 免费在线观看黄色网| 国产午夜精品一区二区三区| 亚洲毛片在线播放| 日韩小视频在线观看专区| 中国a一片一级一片| 黄色成人在线播放| 久久久久久久久久久97| 亚洲欧洲国产日韩| www.黄色在线| proumb性欧美在线观看| 色哟哟在线观看视频| 久久精品噜噜噜成人av农村| 欧美性猛交久久久乱大交小说 | 在线亚洲a色| 国产精品一 二 三| 久久视频免费| 91久久精品国产91性色| 美女视频一区| 国产精品毛片a∨一区二区三区|国 | 亚洲熟妇无码一区二区三区| 欧美午夜一区| 久久久久福利视频| 国产精品sm| 久久久天堂国产精品| 欧美视频一区| 日韩一级片免费视频| 国语精品一区| 国产va亚洲va在线va| 欧美天堂亚洲电影院在线观看 | 国产视频一区欧美| 黄色片网址在线观看| 亚洲国产精品一区制服丝袜| 欧美视频在线观看视频| 一区二区自拍| 婷婷五月综合缴情在线视频| 亚洲久久一区二区| 亚洲熟妇无码一区二区三区| 亚洲精品孕妇| 欧美日韩二三区| 久久先锋资源| 国产一级做a爰片久久| 久久丁香综合五月国产三级网站| 黄色小视频免费网站| 国产高清不卡一区二区| 97中文字幕在线观看| 99精品视频在线观看| 久久精品国产亚洲av久| 欧美国产日韩a欧美在线观看| 老司机深夜福利网站| 亚洲免费观看视频| 久草成人在线视频| 激情久久av一区av二区av三区| 依依成人综合网| 欧美在线短视频| 国产精品视频在线观看免费| 欧美r级在线观看| 四虎精品成人免费网站| 中文字幕亚洲国产| a级影片在线| 高清欧美性猛交xxxx黑人猛交| 午夜欧美激情| 国产日本欧美一区二区三区在线| 免费观看性欧美大片无片| 久久久精品国产一区二区三区| 国产麻豆一区二区三区精品视频| 在线亚洲美日韩| 激情综合久久| 麻豆三级在线观看| 成人美女视频在线观看18| 中国黄色a级片| 亚洲欧美影音先锋| 日韩精品在线观看免费| 欧美人与性动xxxx| 人妻视频一区二区三区| 在线观看中文字幕亚洲| 成人影院在线播放| 国产精品国产福利国产秒拍| 在线日韩成人| 亚洲7777| 一本色道久久综合| 国内外成人免费在线视频| 成人国产精品免费观看| 国产又粗又猛又爽又黄的视频小说| 亚洲专区一二三| 一区二区视频免费| 亚洲精品按摩视频| 久久精品视频观看| 国产91热爆ts人妖在线| 6080亚洲理论片在线观看| 亚洲国产精品一区在线观看不卡| 伊人成年综合电影网| 亚洲最大天堂网| 久久嫩草精品久久久精品| 欧美黄片一区二区三区| 欧美色电影在线| 欧洲视频在线免费观看| 欧美疯狂性受xxxxx另类| 国产极品嫩模在线观看91精品| 国产一区在线观| 你懂的国产精品| 亚洲精品自拍网| 久久久精品综合| 精品无码人妻一区二区三| 在线播放一区二区三区| av午夜在线| 国产99久久精品一区二区永久免费| 亚洲视频一起| 国产日韩第一页| 美女一区二区视频| 亚洲激情视频小说| 欧美日韩亚洲一区二区三区| 欧美天堂在线视频| 九九综合九九综合| 国产一区二区三区精品在线观看| 四虎影院一区二区三区| 巨乳诱惑日韩免费av| 中文字幕 亚洲一区| 亚洲成人免费av| 国产成人三级在线观看视频| 欧美乱妇40p| 韩国一区二区三区视频| 日本黄色播放器| 麻豆精品精品国产自在97香蕉| 男人的天堂官网| 日本韩国精品一区二区在线观看| 免费在线超碰| 日韩av免费网站| 国产一区二区三区四区| 丁香婷婷激情网| 中文字幕免费在线观看视频一区| 无码人妻精品一区二区三区蜜桃91| 日韩国产精品一区| 国内激情视频在线观看| 鲁丝一区二区三区免费| 在线亚洲欧美| 女~淫辱の触手3d动漫| 色哟哟欧美精品| 成人动漫在线播放| 国产精品热视频| 天堂美国久久| wwwxxx色| 亚洲电影一区二区三区| 手机亚洲第一页| 国产精品h在线观看| 日韩欧美自拍| 天天操精品视频| 亚洲一区二区在线免费看| 婷婷丁香花五月天| 国产成人91久久精品| 日本一区二区免费高清| 欧美高清精品一区二区| 亚洲福利视频导航| 男人天堂网在线观看| 国产精品在线看| 欧美视频成人| 舐め犯し波多野结衣在线观看| 欧美亚洲一区二区在线| 成人日韩欧美| 久久福利电影| 蜜臀av一级做a爰片久久| 91 在线视频| 亚洲精品国精品久久99热 | 欧美成人tv| 午夜一区二区三区免费| 欧美色网站导航| 在线电影福利片| 免费久久久一本精品久久区| 免费观看30秒视频久久| 久久久久亚洲av片无码下载蜜桃| 日韩精品中文字幕在线播放| www一区二区三区| 国内外成人激情视频| 一色桃子久久精品亚洲| 少妇人妻精品一区二区三区| 国产精品视频在线播放| 国产精品啊v在线| 精品国产aaa| 亚洲国产成人在线播放| 国产精品亲子伦av一区二区三区| 日韩黄色片在线| 国产精品日日摸夜夜摸av| 免费国产精品视频| 国产日韩精品在线|