日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > CSS >内容正文

CSS

canvas换图时候会闪烁_Canvas实现图片上标注、缩放、移动和保存历史状态,纯干货(附CSS 3变化公式)...

發布時間:2023/12/1 CSS 59 豆豆
生活随笔 收集整理的這篇文章主要介紹了 canvas换图时候会闪烁_Canvas实现图片上标注、缩放、移动和保存历史状态,纯干货(附CSS 3变化公式)... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

(給前端大學加星標,提升前端技能.)

作者:zhcxk1998

https://juejin.im/user/5d4304bdf265da03d15531dc

哈哈哈俺又來啦,這次帶來的是canvas實現一些畫布功能的文章,希望大家喜歡!這個css3變化公式可以適用于平常我們使用的transform屬性或者是移動端我們縮放地圖啊之類的都可以喲!

前言

因為也是大三了,最近俺也在找實習,之前有一個自己的小項目:

https://github.com/zhcxk1998/School-Partners

面試官說可以往深層次思考一下,或許加一些新的功能來增加項目的難度,他提了幾個建議,其中一個就是試卷在線批閱,老師可以在上面對作業進行批注,圈圈點點等俺當天晚上就開始研究這個東東哈哈哈,終于被我研究出來啦!

采用的是canvas繪制畫筆,由css3的transform屬性來進行平移與縮放,因為呢考慮到如果用canvas的drawImage或者scale等屬性進行變化,生成出來的圖片也會有影響,想著直接css3變化,canvas用來做畫筆等功能。大佬們有何妙招,在評論區指點指點!

(希望大家可以留下寶貴的贊與star嘻嘻)

效果預覽

動圖是放cdn的,如果訪問不了,可以登錄在線嘗試嘗試:http://test.algbb.cn/#/admin/content/mark-paper

公式推導

如果不想看公式如何推導,可以直接跳過看后面的具體實現~

1.坐標轉換公式

轉換公式介紹

其實一開始也是想在網上找一下有沒有相關的資料,但是可惜找不到,所以就自己慢慢的推出來了。我就舉一下橫坐標的例子吧!

通用公式

這個公式是表示,通過公式來將鼠標按下的坐標轉換為畫布中的相對坐標,這一點尤為重要

(transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX

參數解釋

transformOrigin: transform變化的基點(通過這個屬性來控制元素以哪里進行變化)downX: 鼠標按下的坐標(注意,用的時候需要減去容器左偏移距離,因為我們要的是相對于容器的坐標)scale: 縮放倍數,默認為1translateX: 平移的距離

推導過程

這個公式的話,其實就比較通用,可以用在別的利用到transform屬性的場景,至于怎么推導的話,我是用的笨辦法

具體的測試代碼,放在文末,需要自取~

1. 先做出兩個相同的元素,然后標記上坐標,并且設置容器屬性overflow:hidden來隱藏溢出內容

ok,現在就有兩個一樣的矩陣啦,我們為他標記上一些紅點,然后我們對左邊的進行css3的樣式變化transform

矩形的寬高是360px * 360px的,我們定義一下他的變化屬性,變化基點選擇正中心,放大3倍

// csstransform-origin: 180px 180px;transform: scale(3, 3);

得到如下結果

ok,我們現在對比一下上面的結果,就會發現,放大3倍的時候,恰好是中間黑色方塊占據了全部寬度。接下來我們就可以對這些點與原先沒有進行變化(右邊)的矩形進行對比就可以得到他們坐標的關系啦

2. 開始對兩個坐標進行對比,然后推出公式

現在舉一個簡單的例子吧,例如我們算一下左上角的坐標(現在已經標記為黃色了)

其實我們其實就可以直接心算出來坐標的關系啦
(這里左邊計算坐標的值是我們鼠標按下的坐標)
(這里左邊計算坐標的值是我們鼠標按下的坐標)
(這里左邊計算坐標的值是我們鼠標按下的坐標)

  • 因為寬高是360px,所以分成3等份,每份寬度是120px

  • 因為變化之后容器的寬高是不變的,變化的只有矩形本身

  • 我們可以得出左邊的黃色標記坐標是x:120 y:0,右邊的黃色標記為x:160 y:120(這個其實肉眼看應該就能看出來了,實在不行可以用紙筆算一算)

這個坐標可能有點特殊,我們再換幾個來計算計算(根據特殊推一般)

  • 藍色標記:左邊:x:120 y:120,右邊:x: 160 y:160

  • 綠色標記:左邊:x: 240 y:240,右邊:x: 200: y:200

好了,我們差不多已經可以拿到坐標之間的關系了,我們可以列一個表

還覺得不放心?我們可以換一下,縮放倍數與容器寬高等進行計算

不知道大家有沒有感覺呢,然后我們就可以慢慢根據坐標推出通用的公式啦

(transformOrigin - downX) / scale * (scale-1) + down - translateX = point

當然,我們或許還有這個translateX沒有嘗試,這個就比較簡單一點了,腦內模擬一下,就知道我們可以減去位移的距離就ok啦。我們測試一下

我們先修改一下樣式,新增一下位移的距離

transform-origin: 180px 180px;transform: scale(3, 3) translate(-40px,-40px);

還是我們上面的狀態,ok,我們現在藍色跟綠色的標記還是一一對應的,那我們看看現在的坐標情況

  • 藍色:左邊:x:0 y:0,右邊:x:160 y:160

  • 綠色:左邊:x:120 y:120,右邊:x:200 y:200

我們分別運用公式算一下出來的坐標是怎么樣的(以下為經過坐標換算)

  • 藍色:左邊:x:120 y:120,右邊:x:160 y:160

  • 綠色:左邊:x:160 y:160,右邊:x:200 y:200

不難發現,我們其實就相差了與位移距離translateX/translateY的差值,所以,我們只需要減去位移的距離就可以完美的進行坐標轉換啦

測試公式

根據上面的公式,我們可以簡單測試一下!這個公式到底能不能生效!!!

我們直接沿用上面的demo,測試一下如果元素進行了變化,我們鼠標點下的地方生成一個標記,位置是否顯示正確。看起來很ok啊(手動滑稽)

const wrap = document.getElementById('wrap')wrap.onmousedown = function (e) { const downX = e.pageX - wrap.offsetLeft const downY = e.pageY - wrap.offsetTop const scale = 3 const translateX = -40 const translateY = -40 const transformOriginX = 180 const transformOriginY = 180 const dot = document.getElementById('dot') dot.style.left = (transformOriginX - downX) / scale * (scale - 1) + downX - translateX + 'px' dot.style.top = (transformOriginY - downY) / scale * (scale - 1) + downY - translateY + 'px'}

可能有人會問,為什么要減去這個offsetLeft跟offsetTop呢,因為我們上面反復強調,我們計算的是鼠標點擊的坐標,而這個坐標還是相對于我們展示容器的坐標,所以我們要減去容器本身的偏移量才行。

組件設計

既然demo啥的都已經測試了ok了,我們接下來就逐一分析一下這個組件應該咋設計好呢(目前仍為低配版,之后再進行優化完善)

1. 基本的畫布構成

我們先簡單分析一下這個構成吧,其實主要就是一個畫布的容器,右邊一個工具欄,僅此而已

大體就這樣子啦!

ref={canvasRef} className="mark-paper__canvas">

很可惜,這個東東與您的電腦不搭!

我們唯一需要的一點就是,容器需要設置屬性overflow: hidden用來隱藏內部canvas畫布溢出的內容,也就是說,我們要控制我們可視的區域。同時我們需要動態獲取容器寬高來為canvas設置尺寸

2. 初始化canvas畫布與填充圖片

我們可以弄個方法來初始化并且填充畫布,以下截取主要部分,其實就是為canvas畫布設置尺寸與填充我們的圖片

const fillImage = async () => { // 此處省略... const img: HTMLImageElement = new Image() img.src = await getURLBase64(fillImageSrc) img.onload = () => { canvas.width = img.width canvas.height = img.height context.drawImage(img, 0, 0) // 設置變化基點,為畫布容器中央 canvas.style.transformOrigin = `${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px` // 清除上一次變化的效果 canvas.style.transform = '' }}

3. 監聽canvas畫布的各種鼠標事件

這個控制移動的話,我們首先可以弄一個方法來監聽畫布鼠標的各種事件,可以區分不同的模式來進行不同的事件處理

const handleCanvas = () => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!context || !wrap) return // 清除上一次設置的監聽,以防獲取參數錯誤 wrap.onmousedown = null wrap.onmousedown = function (event: MouseEvent) { const downX: number = event.pageX const downY: number = event.pageY // 區分我們現在選擇的鼠標模式:移動、畫筆、橡皮擦 switch (mouseMode) { case MOVE_MODE: handleMoveMode(downX, downY) break case LINE_MODE: handleLineMode(downX, downY) break case ERASER_MODE: handleEraserMode(downX, downY) break default: break } }

4. 實現畫布移動

這個就比較好辦啦,我們只需要利用鼠標按下的坐標,和我們拖動的距離就可以實現畫布的移動啦,因為涉及到每次移動都需要計算最新的位移距離,我們可以定義幾個變量來進行計算。

這里監聽的是容器的鼠標事件,而不是canvas畫布的事件,因為這樣子我們可以再移動超過邊界的時候也可以進行移動操作

簡單的總結一下:

  • 傳入鼠標按下的坐標

  • 計算當前位移距離,并更新css變化效果

  • 鼠標抬起時更新最新的位移狀態

// 定義一些變量,來保存當前/最新的移動狀態// 當前位移的距離const translatePointXRef: MutableRefObject = useRef(0)const translatePointYRef: MutableRefObject = useRef(0)// 上一次位移結束的位移距離const fillStartPointXRef: MutableRefObject = useRef(0)const fillStartPointYRef: MutableRefObject = useRef(0)// 移動時候的監聽函數const handleMoveMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const { current: fillStartPointX } = fillStartPointXRef const { current: fillStartPointY } = fillStartPointYRef if (!canvas || !wrap || mouseMode !== 0) return // 為容器添加移動事件,可以在空白處移動圖片 wrap.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX const moveY: number = event.pageY // 更新現在的位移距離,值為:上一次位移結束的坐標+移動的距離 translatePointXRef.current = fillStartPointX + (moveX - downX) translatePointYRef.current = fillStartPointY + (moveY - downY) // 更新畫布的css變化 canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)` } wrap.onmouseup = (event: MouseEvent) => { const upX: number = event.pageX const upY: number = event.pageY // 取消事件監聽 wrap.onmousemove = null wrap.onmouseup = null; // 鼠標抬起時候,更新“上一次唯一結束的坐標” fillStartPointXRef.current = fillStartPointX + (upX - downX) fillStartPointYRef.current = fillStartPointY + (upY - downY) }}

5. 實現畫布縮放

畫布縮放我主要通過右側的滑動條以及鼠標滾輪來實現,首先我們再監聽畫布鼠標事件的函數中加一下監聽滾輪的事件

總結一下:

  • 監聽鼠標滾輪的變化

  • 更新縮放倍數,并改變樣式

// 監聽鼠標滾輪,更新畫布縮放倍數const handleCanvas = () => { const { current: wrap } = wrapRef // 省略一萬字... wrap.onwheel = null wrap.onwheel = (e: MouseWheelEvent) => { const { deltaY } = e // 這里要注意一下,我是0.1來遞增遞減,但是因為JS使用IEEE 754,來計算,所以精度有問題,我們自己處理一下 const newScale: number = deltaY > 0 ? (canvasScale * 10 - 0.1 * 10) / 10 : (canvasScale * 10 + 0.1 * 10) / 10 if (newScale < 0.1 || newScale > 2) return setCanvasScale(newScale) }}// 監聽滑動條來控制縮放 min={0.1} max={2.01} step={0.1} value={canvasScale} tipFormatter={(value) => `${(value).toFixed(2)}x`} onChange={handleScaleChange} />const handleScaleChange = (value: number) => { setCanvasScale(value)}

接著我們使用hooks的副作用函數,依賴于畫布縮放倍數來進行樣式的更新

//監聽縮放畫布useEffect(() => { const { current: canvas } = canvasRef const { current: translatePointX } = translatePointXRef const { current: translatePointY } = translatePointYRef canvas && (canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointX}px,${translatePointY}px)`)}, [canvasScale])

6. 實現畫筆繪制

這個就需要用到我們之前推導出來的公式啦!因為呢,仔細想一下,如果我們縮放位移之后,我們鼠標按下的位置,他的坐標可能就相對于畫布來說會有變化,所以我們需要轉換一下才能進行鼠標按下的位置與畫布的位置一一對應的效果

稍微總結一下:

  • 傳入鼠標按下的坐標

  • 通過公式轉換,開始在對應坐標下繪制

  • 鼠標抬起時,取消事件監聽

// 利用公式轉換一下坐標const generateLinePoint = (x: number, y: number) => { const { current: wrap } = wrapRef const { current: translatePointX } = translatePointXRef const { current: translatePointY } = translatePointYRef const wrapWidth: number = wrap?.offsetWidth || 0 const wrapHeight: number = wrap?.offsetHeight || 0 // 縮放位移坐標變化規律 // (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY return { pointX, pointY }}// 監聽鼠標畫筆事件const handleLineMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !wrap || !context) return const offsetLeft: number = canvas.offsetLeft const offsetTop: number = canvas.offsetTop // 減去畫布偏移的距離(以畫布為基準進行計算坐標) downX = downX - offsetLeft downY = downY - offsetTop const { pointX, pointY } = generateLinePoint(downX, downY) context.globalCompositeOperation = "source-over" context.beginPath() // 設置畫筆起點 context.moveTo(pointX, pointY) canvas.onmousemove = null canvas.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX - offsetLeft const moveY: number = event.pageY - offsetTop const { pointX, pointY } = generateLinePoint(moveX, moveY) // 開始繪制畫筆線條~ context.lineTo(pointX, pointY) context.stroke() } canvas.onmouseup = () => { context.closePath() canvas.onmousemove = null canvas.onmouseup = null }}

7. 橡皮擦的實現

橡皮擦目前還有點問題,現在的話是通過將canvas畫布的背景圖片 + globalCompositeOperation這個屬性來模擬橡皮擦的實現,不過,這時候圖片生成出來之后,橡皮擦的痕跡會變成白色,而不是透明

此步驟與畫筆實現差不多,只有一點點小變動

  • 設置屬性context.globalCompositeOperation = "destination-out"

// 目前橡皮擦還有點問題,前端顯示正常,保存圖片下來,擦除的痕跡會變成白色const handleEraserMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !wrap || !context) return const offsetLeft: number = canvas.offsetLeft const offsetTop: number = canvas.offsetTop downX = downX - offsetLeft downY = downY - offsetTop const { pointX, pointY } = generateLinePoint(downX, downY) context.beginPath() context.moveTo(pointX, pointY) canvas.onmousemove = null canvas.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX - offsetLeft const moveY: number = event.pageY - offsetTop const { pointX, pointY } = generateLinePoint(moveX, moveY) context.globalCompositeOperation = "destination-out" context.lineWidth = lineWidth context.lineTo(pointX, pointY) context.stroke() } canvas.onmouseup = () => { context.closePath() canvas.onmousemove = null canvas.onmouseup = null }}

8. 撤銷與恢復的功能實現

這個的話,我們首先需要了解常見的撤銷與恢復的功能的邏輯分幾種情況吧

  • 若當前狀態處于第一個位置,則不允許撤銷

  • 若當前狀態處于最后一個位置,則不允許恢復

  • 如果當前撤銷了,然而更新了狀態,則取當前狀態為最新的狀態(也就是說不允許恢復了,這個剛更新的狀態就是最新的)

畫布狀態的更新

所以我們需要設置一些變量來存,狀態列表,與當前畫筆的狀態下標

// 定義參數存東東const canvasHistroyListRef: MutableRefObject = useRef([])const [canvasCurrentHistory, setCanvasCurrentHistory] = useState(0)

我們還需要在初始化canvas的時候,我們就添加入當前的狀態存入列表中,作為最先開始的空畫布狀態

const fillImage = async () => { // 省略一萬字... img.src = await getURLBase64(fillImageSrc) img.onload = () => { const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height) canvasHistroyListRef.current = [] canvasHistroyListRef.current.push(imageData) setCanvasCurrentHistory(1) }}

然后我們就實現一下,畫筆更新時候,我們也需要將當前的狀態添加入畫筆狀態列表,并且更新當前狀態對應的下標,還需要處理一下一些細節

總結一下:

  • 鼠標抬起時,獲取當前canvas畫布狀態

  • 添加進狀態列表中,并且更新狀態下標

  • 如果當前處于撤銷狀態,若使用畫筆更新狀態,則將當前的最為最新的狀態,原先位置之后的狀態全部清空

const handleLineMode = (downX: number, downY: number) => { // 省略一萬字... canvas.onmouseup = () => { const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height) // 如果此時處于撤銷狀態,此時再使用畫筆,則將之后的狀態清空,以剛畫的作為最新的畫布狀態 if (canvasCurrentHistory < canvasHistroyListRef.current.length) { canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory) } canvasHistroyListRef.current.push(imageData) setCanvasCurrentHistory(canvasCurrentHistory + 1) context.closePath() canvas.onmousemove = null canvas.onmouseup = null }}

畫布狀態的撤銷與恢復

ok,其實現在關于畫布狀態的更新,我們已經完成了。接下來我們需要處理一下狀態的撤銷與恢復的功能啦

我們先定義一下這個工具欄吧

然后我們設置對應的事件,分別是撤銷,恢復,與清空,其實都很容易看懂,最多就是處理一下邊界情況。

const handleRollBack = () => { const isFirstHistory: boolean = canvasCurrentHistory === 1 if (isFirstHistory) return setCanvasCurrentHistory(canvasCurrentHistory - 1)}const handleRollForward = () => { const { current: canvasHistroyList } = canvasHistroyListRef const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length if (isLastHistory) return setCanvasCurrentHistory(canvasCurrentHistory + 1)}const handleClearCanvasClick = () => { const { current: canvas } = canvasRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !context || canvasCurrentHistory === 0) return // 清空畫布歷史 canvasHistroyListRef.current = [canvasHistroyListRef.current[0]] setCanvasCurrentHistory(1) message.success('畫布清除成功!')}

事件設置好之后,我們就可以開始監聽一下這個canvasCurrentHistory當前狀態下標,使用副作用函數進行處理

useEffect(() => { const { current: canvas } = canvasRef const { current: canvasHistroyList } = canvasHistroyListRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !context || canvasCurrentHistory === 0) return context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0)}, [canvasCurrentHistory])

為canvas畫布填充圖像信息!

這樣就大功告成啦!!!

9. 實現鼠標圖標的變化

我們簡單的處理一下,畫筆模式則是畫筆的圖標,橡皮擦模式下鼠標是橡皮擦,移動模式下就是普通的移動圖標

切換模式時候,設置一下不同的圖標

const handleMouseModeChange = (event: RadioChangeEvent) => { const { target: { value } } = event const { current: canvas } = canvasRef const { current: wrap } = wrapRef setmouseMode(value) if (!canvas || !wrap) return switch (value) { case MOVE_MODE: canvas.style.cursor = 'move' wrap.style.cursor = 'move' break case LINE_MODE: canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer` wrap.style.cursor = 'default' break case ERASER_MODE: message.warning('橡皮擦功能尚未完善,保存圖片會出現錯誤') canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer` wrap.style.cursor = 'default' break default: canvas.style.cursor = 'default' wrap.style.cursor = 'default' break }}

10. 切換圖片

現在的話只是一個demo狀態,通過點擊選擇框,切換不同的圖片

// 重置變換參數,重新繪制圖片useEffect(() => { setIsLoading(true) translatePointXRef.current = 0 translatePointYRef.current = 0 fillStartPointXRef.current = 0 fillStartPointYRef.current = 0 setCanvasScale(1) fillImage()}, [fillImageSrc])const handlePaperChange = (value: string) => { const fillImageList = { 'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg', 'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png', 'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png', } setFillImageSrc(fillImageList[value])}

注意事項

注意容器的偏移量

我們需要注意一下,因為公式中的downX是相對容器的坐標,也就是說,我們需要減去容器的偏移量,這種情況會出現在使用了margin等參數,或者說上方或者左側有別的元素的情況

我們輸出一下我們紅色的元素的offsetLeft等屬性,會發現他是已經本身就有50的偏移量了,我們計算鼠標點擊的坐標的時候就要減去這一部分的偏移量

window.onload = function () { const test = document.getElementById('test') console.log(`offsetLeft: ${test.offsetLeft}, offsetHeight: ${test.offsetTop}`)}html,body { margin: 0; padding: 0;}#test { width: 50px; height: 50px; margin-left: 50px; background: red;}

注意父組件使用relative相對布局的情況

假如我們現在有一種這種的布局,打印紅色元素的偏移量,看起來都挺正常的

但是如果我們目標元素的父元素(也就是黃色部分)設置relative相對布局

.wrap { position: relative; width: 400px; height: 300px; background: yellow;}

這時候我們打印出來的偏移量會是多少呢

兩次答案不一樣啊,因為我們的偏移量是根據相對位置來計算的,如果父容器使用相對布局,則會影響我們子元素的偏移量

組件代碼(低配版)

import React, { FC, ComponentType, useEffect, useRef, RefObject, useState, MutableRefObject } from 'react'import { CustomBreadcrumb } from '@/admin/components'import { RouteComponentProps } from 'react-router-dom';import { FormComponentProps } from 'antd/lib/form';import { Slider, Radio, Button, Tooltip, Icon, Select, Spin, message, Popconfirm} from 'antd';import './index.scss'import { RadioChangeEvent } from 'antd/lib/radio';import { getURLBase64 } from '@/admin/utils/getURLBase64'const { Option, OptGroup } = Select;type MarkPaperProps = RouteComponentProps & FormComponentPropsconst MarkPaper: FC = (props: MarkPaperProps) => { const MOVE_MODE: number = 0 const LINE_MODE: number = 1 const ERASER_MODE: number = 2 const canvasRef: RefObject = useRef(null) const containerRef: RefObject = useRef(null) const wrapRef: RefObject = useRef(null) const translatePointXRef: MutableRefObject = useRef(0) const translatePointYRef: MutableRefObject = useRef(0) const fillStartPointXRef: MutableRefObject = useRef(0) const fillStartPointYRef: MutableRefObject = useRef(0) const canvasHistroyListRef: MutableRefObject = useRef([]) const [lineColor, setLineColor] = useState('#fa4b2a') const [fillImageSrc, setFillImageSrc] = useState('') const [mouseMode, setmouseMode] = useState(MOVE_MODE) const [lineWidth, setLineWidth] = useState(5) const [canvasScale, setCanvasScale] = useState(1) const [isLoading, setIsLoading] = useState(false) const [canvasCurrentHistory, setCanvasCurrentHistory] = useState(0) useEffect(() => { setFillImageSrc('http://cdn.algbb.cn/test/canvasTest.jpg') }, []) // 重置變換參數,重新繪制圖片 useEffect(() => { setIsLoading(true) translatePointXRef.current = 0 translatePointYRef.current = 0 fillStartPointXRef.current = 0 fillStartPointYRef.current = 0 setCanvasScale(1) fillImage() }, [fillImageSrc]) // 畫布參數變動時,重新監聽canvas useEffect(() => { handleCanvas() }, [mouseMode, canvasScale, canvasCurrentHistory]) // 監聽畫筆顏色變化 useEffect(() => { const { current: canvas } = canvasRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!context) return context.strokeStyle = lineColor context.lineWidth = lineWidth context.lineJoin = 'round' context.lineCap = 'round' }, [lineWidth, lineColor]) //監聽縮放畫布 useEffect(() => { const { current: canvas } = canvasRef const { current: translatePointX } = translatePointXRef const { current: translatePointY } = translatePointYRef canvas && (canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointX}px,${translatePointY}px)`) }, [canvasScale]) useEffect(() => { const { current: canvas } = canvasRef const { current: canvasHistroyList } = canvasHistroyListRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !context || canvasCurrentHistory === 0) return context?.putImageData(canvasHistroyList[canvasCurrentHistory - 1], 0, 0) }, [canvasCurrentHistory]) const fillImage = async () => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') const img: HTMLImageElement = new Image() if (!canvas || !wrap || !context) return img.src = await getURLBase64(fillImageSrc) img.onload = () => { // 取中間渲染圖片 // const centerX: number = canvas && canvas.width / 2 - img.width / 2 || 0 // const centerY: number = canvas && canvas.height / 2 - img.height / 2 || 0 canvas.width = img.width canvas.height = img.height // 背景設置為圖片,橡皮擦的效果才能出來 canvas.style.background = `url(${img.src})` context.drawImage(img, 0, 0) context.strokeStyle = lineColor context.lineWidth = lineWidth context.lineJoin = 'round' context.lineCap = 'round' // 設置變化基點,為畫布容器中央 canvas.style.transformOrigin = `${wrap?.offsetWidth / 2}px ${wrap?.offsetHeight / 2}px` // 清除上一次變化的效果 canvas.style.transform = '' const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height) canvasHistroyListRef.current = [] canvasHistroyListRef.current.push(imageData) // canvasCurrentHistoryRef.current = 1 setCanvasCurrentHistory(1) setTimeout(() => { setIsLoading(false) }, 500) } } const generateLinePoint = (x: number, y: number) => { const { current: wrap } = wrapRef const { current: translatePointX } = translatePointXRef const { current: translatePointY } = translatePointYRef const wrapWidth: number = wrap?.offsetWidth || 0 const wrapHeight: number = wrap?.offsetHeight || 0 // 縮放位移坐標變化規律 // (transformOrigin - downX) / scale * (scale-1) + downX - translateX = pointX const pointX: number = ((wrapWidth / 2) - x) / canvasScale * (canvasScale - 1) + x - translatePointX const pointY: number = ((wrapHeight / 2) - y) / canvasScale * (canvasScale - 1) + y - translatePointY return { pointX, pointY } } const handleLineMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !wrap || !context) return const offsetLeft: number = canvas.offsetLeft const offsetTop: number = canvas.offsetTop // 減去畫布偏移的距離(以畫布為基準進行計算坐標) downX = downX - offsetLeft downY = downY - offsetTop const { pointX, pointY } = generateLinePoint(downX, downY) context.globalCompositeOperation = "source-over" context.beginPath() context.moveTo(pointX, pointY) canvas.onmousemove = null canvas.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX - offsetLeft const moveY: number = event.pageY - offsetTop const { pointX, pointY } = generateLinePoint(moveX, moveY) context.lineTo(pointX, pointY) context.stroke() } canvas.onmouseup = () => { const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height) // 如果此時處于撤銷狀態,此時再使用畫筆,則將之后的狀態清空,以剛畫的作為最新的畫布狀態 if (canvasCurrentHistory < canvasHistroyListRef.current.length) { canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory) } canvasHistroyListRef.current.push(imageData) setCanvasCurrentHistory(canvasCurrentHistory + 1) context.closePath() canvas.onmousemove = null canvas.onmouseup = null } } const handleMoveMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const { current: fillStartPointX } = fillStartPointXRef const { current: fillStartPointY } = fillStartPointYRef if (!canvas || !wrap || mouseMode !== 0) return // 為容器添加移動事件,可以在空白處移動圖片 wrap.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX const moveY: number = event.pageY translatePointXRef.current = fillStartPointX + (moveX - downX) translatePointYRef.current = fillStartPointY + (moveY - downY) canvas.style.transform = `scale(${canvasScale},${canvasScale}) translate(${translatePointXRef.current}px,${translatePointYRef.current}px)` } wrap.onmouseup = (event: MouseEvent) => { const upX: number = event.pageX const upY: number = event.pageY wrap.onmousemove = null wrap.onmouseup = null; fillStartPointXRef.current = fillStartPointX + (upX - downX) fillStartPointYRef.current = fillStartPointY + (upY - downY) } } // 目前橡皮擦還有點問題,前端顯示正常,保存圖片下來,擦除的痕跡會變成白色 const handleEraserMode = (downX: number, downY: number) => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !wrap || !context) return const offsetLeft: number = canvas.offsetLeft const offsetTop: number = canvas.offsetTop downX = downX - offsetLeft downY = downY - offsetTop const { pointX, pointY } = generateLinePoint(downX, downY) context.beginPath() context.moveTo(pointX, pointY) canvas.onmousemove = null canvas.onmousemove = (event: MouseEvent) => { const moveX: number = event.pageX - offsetLeft const moveY: number = event.pageY - offsetTop const { pointX, pointY } = generateLinePoint(moveX, moveY) context.globalCompositeOperation = "destination-out" context.lineWidth = lineWidth context.lineTo(pointX, pointY) context.stroke() } canvas.onmouseup = () => { const imageData: ImageData = context.getImageData(0, 0, canvas.width, canvas.height) if (canvasCurrentHistory < canvasHistroyListRef.current.length) { canvasHistroyListRef.current = canvasHistroyListRef.current.slice(0, canvasCurrentHistory) } canvasHistroyListRef.current.push(imageData) setCanvasCurrentHistory(canvasCurrentHistory + 1) context.closePath() canvas.onmousemove = null canvas.onmouseup = null } } const handleCanvas = () => { const { current: canvas } = canvasRef const { current: wrap } = wrapRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!context || !wrap) return // 清除上一次設置的監聽,以防獲取參數錯誤 wrap.onmousedown = null wrap.onmousedown = function (event: MouseEvent) { const downX: number = event.pageX const downY: number = event.pageY switch (mouseMode) { case MOVE_MODE: handleMoveMode(downX, downY) break case LINE_MODE: handleLineMode(downX, downY) break case ERASER_MODE: handleEraserMode(downX, downY) break default: break } } wrap.onwheel = null wrap.onwheel = (e: MouseWheelEvent) => { const { deltaY } = e const newScale: number = deltaY > 0 ? (canvasScale * 10 - 0.1 * 10) / 10 : (canvasScale * 10 + 0.1 * 10) / 10 if (newScale < 0.1 || newScale > 2) return setCanvasScale(newScale) } } const handleScaleChange = (value: number) => { setCanvasScale(value) } const handleLineWidthChange = (value: number) => { setLineWidth(value) } const handleColorChange = (color: string) => { setLineColor(color) } const handleMouseModeChange = (event: RadioChangeEvent) => { const { target: { value } } = event const { current: canvas } = canvasRef const { current: wrap } = wrapRef setmouseMode(value) if (!canvas || !wrap) return switch (value) { case MOVE_MODE: canvas.style.cursor = 'move' wrap.style.cursor = 'move' break case LINE_MODE: canvas.style.cursor = `url('http://cdn.algbb.cn/pencil.ico') 6 26, pointer` wrap.style.cursor = 'default' break case ERASER_MODE: message.warning('橡皮擦功能尚未完善,保存圖片會出現錯誤') canvas.style.cursor = `url('http://cdn.algbb.cn/eraser.ico') 6 26, pointer` wrap.style.cursor = 'default' break default: canvas.style.cursor = 'default' wrap.style.cursor = 'default' break } } const handleSaveClick = () => { const { current: canvas } = canvasRef // 可存入數據庫或是直接生成圖片 console.log(canvas?.toDataURL()) } const handlePaperChange = (value: string) => { const fillImageList = { 'xueshengjia': 'http://cdn.algbb.cn/test/canvasTest.jpg', 'xueshengyi': 'http://cdn.algbb.cn/test/canvasTest2.png', 'xueshengbing': 'http://cdn.algbb.cn/emoji/30.png', } setFillImageSrc(fillImageList[value]) } const handleRollBack = () => { const isFirstHistory: boolean = canvasCurrentHistory === 1 if (isFirstHistory) return setCanvasCurrentHistory(canvasCurrentHistory - 1) } const handleRollForward = () => { const { current: canvasHistroyList } = canvasHistroyListRef const isLastHistory: boolean = canvasCurrentHistory === canvasHistroyList.length if (isLastHistory) return setCanvasCurrentHistory(canvasCurrentHistory + 1) } const handleClearCanvasClick = () => { const { current: canvas } = canvasRef const context: CanvasRenderingContext2D | undefined | null = canvas?.getContext('2d') if (!canvas || !context || canvasCurrentHistory === 0) return // 清空畫布歷史 canvasHistroyListRef.current = [canvasHistroyListRef.current[0]] setCanvasCurrentHistory(1) message.success('畫布清除成功!') } return ( className="mark-paper__mask" style={{ display: isLoading ? 'flex' : 'none' }} > tip="圖片加載中..." indicator={ />} /> ref={canvasRef} className="mark-paper__canvas">

很可惜,這個東東與您的電腦不搭!

選擇作業: defaultValue="xueshengjia" style={{ width: '100%', margin: '10px 0 20px 0' }} onChange={handlePaperChange} > 學生甲 學生乙 學生丙 畫布操作: className={`icon iconfont icon-chexiao ${canvasCurrentHistory <= 1 && 'disable'}`} onClick={handleRollBack} /> className={`icon iconfont icon-fanhui ${canvasCurrentHistory >= canvasHistroyListRef.current.length && 'disable'}`} onClick={handleRollForward} /> onConfirm={handleClearCanvasClick} okText="確定" cancelText="取消" > 畫布縮放: min={0.1} max={2.01} step={0.1} value={canvasScale} tipFormatter={(value) => `${(value).toFixed(2)}x`} onChange={handleScaleChange} /> 畫筆大小: min={1} max={9} value={lineWidth} tipFormatter={(value) => `${value}px`} onChange={handleLineWidthChange} /> 模式選擇: className="radio-group" onChange={handleMouseModeChange} value={mouseMode}> 移動 畫筆 橡皮擦 顏色選擇: {['#fa4b2a', '#ffff00', '#ee00ee', '#1890ff', '#333333', '#ffffff'].map(color => { return ( role="button" className={`color-picker__wrap ${color === lineColor && 'color-picker__wrap--active'}`} style={{ background: color }} onClick={() => handleColorChange(color)} /> ) })} 保存圖片 )}export default MarkPaper as ComponentType

結語

如果這篇東東對大家有所幫助,希望大家可以給我點贊一下鼓勵一下!

或者給俺的項目點個star支持支持吧!

github.com/zhcxk1998/School-Partners

菜雞分析的不到位,還請各位大佬指出俺的不足!阿里嘎多~

分享前端好文,點亮?在看?

總結

以上是生活随笔為你收集整理的canvas换图时候会闪烁_Canvas实现图片上标注、缩放、移动和保存历史状态,纯干货(附CSS 3变化公式)...的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

亚洲乱亚洲乱妇 | 日韩精品视频在线观看免费 | 国产69精品久久久久久久久久 | 九九亚洲视频 | 一区二区精品在线 | 91人人干 | 日韩av中文字幕在线 | 国产精品福利av | 免费观看一区二区三区视频 | 欧美成天堂网地址 | 狠狠躁夜夜躁人人爽超碰97香蕉 | 久久高清视频免费 | 国产精品视频 | 91三级在线观看 | 在线观看中文字幕网站 | 中文字幕av免费在线观看 | 日本xxxx裸体xxxx17| 欧美激情综合五月色丁香小说 | 欧洲一区二区三区精品 | 国产高清视频免费在线观看 | 久久精品国产一区二区 | 国产区久久 | 成人一级在线观看 | 精品免费视频 | 美女黄久久| 在线亚州 | 久久超碰免费 | 亚洲免费视频观看 | 国产精品久久久久久99 | 黄色小说在线观看视频 | 精品久久网 | 国产高清视频在线观看 | 久久国产亚洲 | 在线观看视频一区二区三区 | 九九久久免费 | 99久热在线精品视频观看 | 波多野结衣一区三区 | 99久久久国产精品免费99 | 欧美激情精品久久久久久免费印度 | 国产精品一区二区三区在线播放 | 久精品在线观看 | 久久视频网址 | 日韩高清毛片 | 在线观看你懂的网站 | 久久美女精品 | 天天射天天做 | 亚洲精品在线视频 | 日韩爱爱网站 | 日韩电影一区二区在线观看 | 国内三级在线 | 亚洲另类视频在线 | 干天天| 亚洲91网站 | 国产99亚洲| 国产精品一级在线 | 国产精品九九久久久久久久 | 午夜精品一区二区三区在线 | 91中文视频| 国产精品乱码久久久久久1区2区 | 92中文资源在线 | 国产一区视频免费在线观看 | 久久久久久久免费 | 国产成人久久av | 在线观看中文字幕 | 免费av的网站| 丰满少妇在线观看资源站 | 在线播放日韩av | 婷婷激情在线 | 字幕网资源站中文字幕 | 黄色中文字幕 | 欧美成人视| 国产高清不卡av | 婷婷久久综合网 | 国产色一区| 日韩在线不卡视频 | 高清免费在线视频 | 久久视频免费在线观看 | 国产精品嫩草影院99网站 | 日日夜夜精品免费观看 | 免费成人在线电影 | 国内精品久久久久久久久久清纯 | 欧美乱淫视频 | av品善网 | 99在线精品免费视频九九视 | 99精品视频精品精品视频 | 公开超碰在线 | 香蕉视频网址 | 黄色a在线观看 | 国产精品国产三级国产专区53 | 亚洲精品午夜久久久 | 久久看免费视频 | 在线免费观看黄 | 超碰公开在线观看 | 亚洲日本国产精品 | 国产主播大尺度精品福利免费 | 天堂网av在线 | 精品一区二区精品 | 精品久久久久久久久久国产 | 最新久久久 | 伊人中文在线 | 欧美精品亚州精品 | 国产区久久 | 狠狠色丁香久久婷婷综合丁香 | 五月亚洲综合 | 久久网页| 国产在线色 | 国产二区av | 91亚州| 伊人射| 一级免费av | 在线播放国产一区二区三区 | 韩国av电影在线观看 | 一区二区三区影院 | 午夜 久久 tv | 婷婷丁香花五月天 | 天天干夜夜干 | 免费看的黄色 | 亚洲精品大全 | 久久精品亚洲国产 | 国产亚洲字幕 | 伊色综合久久之综合久久 | 在线观看中文字幕一区二区 | 亚洲免费一级电影 | 久久手机视频 | 成年人国产在线观看 | 久久综合九色综合欧美狠狠 | 国产精品久久久久久久久久免费 | 久久久久久久久亚洲精品 | 97视频总站 | 亚洲.www| 国产美女无遮挡永久免费 | a在线一区 | 免费手机黄色网址 | 狠狠色丁香婷婷综合久久片 | 亚洲免费在线播放视频 | 92国产精品久久久久首页 | 成人国产在线 | 丁香婷婷色综合亚洲电影 | 午夜久操 | 人人射人人澡 | 国产无吗一区二区三区在线欢 | 亚洲视频在线视频 | 九九九热精品 | 美女久久网站 | 国产精品av在线免费观看 | 在线观看www视频 | 一本一道久久a久久综合蜜桃 | 中文字幕在线视频国产 | 日韩欧美国产成人 | 婷婷久久一区二区三区 | 91精品国产乱码久久桃 | 97精品国自产拍在线观看 | 国产亚洲精品v | 亚洲理论片 | 国模精品在线 | 久久免费a | 国产精品久久久久久久久免费看 | 国产精品久久久久永久免费 | 国产一区视频在线播放 | 国产免费观看久久黄 | 日韩欧美亚州 | 在线免费观看黄色小说 | 国产精品视频久久 | 韩日精品在线 | 五月婷婷婷婷婷 | 中文字幕一区二区三区四区久久 | 中文字幕第一页av | 国产精品嫩草影视久久久 | 一区中文字幕电影 | 中文字幕久久久精品 | 久久久久福利视频 | 精品免费久久久久 | 中文字幕精品www乱入免费视频 | 天天色天天艹 | 爱色婷婷 | 国产日韩欧美在线免费观看 | 少妇bbbb | 精品91久久久久 | 日本夜夜草视频网站 | 日韩动漫免费观看高清完整版在线观看 | 中文字幕九九 | 国模一二三区 | 日韩特黄一级欧美毛片特黄 | 在线成人看片 | 亚洲国产精品va在线看黑人动漫 | 最新中文在线视频 | 欧美日韩国产mv | 国产精品99蜜臀久久不卡二区 | 中文字幕 91 | 成人av地址 | 日韩理论在线视频 | 日韩午夜av电影 | 成人精品影视 | 欧美在线视频精品 | 国产小视频你懂的在线 | 欧美激情精品久久 | 国产人成看黄久久久久久久久 | 国产精品乱码久久久久久1区2区 | 九月婷婷人人澡人人添人人爽 | 九九久久精品视频 | 91成年人视频 | 丰满少妇在线观看资源站 | 欧美综合色 | 国产一区二区精品在线 | 中文字幕在线观 | 久久久国产视频 | 毛片888| 久久精品影片 | 手机看片1042 | 久草网视频在线观看 | 久草精品视频在线播放 | 一级片视频在线 | 欧美另类美少妇69xxxx | 最新国产一区二区三区 | 天天操夜夜拍 | 天天鲁一鲁摸一摸爽一爽 | 中文字幕日韩伦理 | 国产精品嫩草影院9 | 欧美日韩视频免费 | 国产成人不卡 | 手机av网站 | 久久久影视 | 99久久精品免费看国产麻豆 | 欧美一级小视频 | jizz999| 成人在线播放免费观看 | 精品国产精品久久一区免费式 | 亚洲最新视频在线 | 99视频在线免费播放 | 中文字幕久久精品一区 | 五月婷婷色播 | 97国产情侣爱久久免费观看 | 在线电影 你懂得 | 99精品毛片 | 久99久久| 亚洲麻豆精品 | 国产精品成人在线观看 | 96av麻豆蜜桃一区二区 | 国产一区观看 | 久久久久久久av麻豆果冻 | 丁香激情五月 | 狠狠色伊人亚洲综合成人 | 亚洲年轻女教师毛茸茸 | 成人av影视在线 | 91精品视频免费看 | 亚洲人成精品久久久久 | 中文字幕色综合网 | 视频一区二区免费 | 天天色天天射综合网 | 久久这里 | 99久久99久久综合 | 又黄又刺激 | 日韩精品一区二区三区不卡 | 久久毛片视频 | 91精品麻豆 | 日韩av快播电影网 | 黄色资源在线观看 | 成年人免费在线观看网站 | 久久99免费观看 | 欧美性极品xxxx娇小 | 懂色av一区二区三区蜜臀 | 欧美成天堂网地址 | 国产免费亚洲高清 | 久久艹国产 | 中文字幕亚洲欧美 | 成人黄大片视频在线观看 | 久久精品小视频 | 五月天久久精品 | 成人久久久精品国产乱码一区二区 | 一区二区三区精品在线视频 | 91在线操| 日韩黄色av网站 | 国产成人精品日本亚洲999 | 西西www4444大胆视频 | 黄色成人免费电影 | 99热这里有| 91麻豆精品国产91久久久使用方法 | 色av婷婷| 六月丁香综合 | 日韩欧美一区二区在线 | 天天干天天在线 | 国产高清av | 日韩剧 | av在线日韩 | 亚洲国产中文字幕在线视频综合 | 天天干天天干天天射 | 国产视| 久久高清av | 在线看毛片网站 | 国产精品美女久久久 | 日日夜夜精品视频 | 九九免费精品视频 | 夜夜躁狠狠燥 | 激情动态 | 中文字幕亚洲综合久久五月天色无吗'' | 国产亚洲精品成人 | 91一区二区在线 | 麻豆免费视频 | 69成人在线 | 丁香视频| 500部大龄熟乱视频 欧美日本三级 | 亚洲精品美女久久久久 | 国产精品美女久久久久久久 | 久久免费国产精品 | 中文国产字幕 | 国产黄色播放 | www.久久免费视频 | 麻豆91网站 | av中文在线影视 | 超碰在线色 | 五月天中文字幕mv在线 | 四虎在线免费观看视频 | 香蕉视频在线免费 | 亚洲精品视频在线免费播放 | 99国产成+人+综合+亚洲 欧美 | 国产亚洲欧洲 | 国产特级毛片aaaaaa高清 | 视频在线观看入口黄最新永久免费国产 | 91丨九色丨高潮 | 亚洲资源视频 | 精品影院一区二区久久久 | 日韩一区精品 | 亚洲精品看片 | 五月激情丁香图片 | 天天综合成人网 | 久草在线视频首页 | 国产成人精品一区二区三区网站观看 | 国产高清永久免费 | 91视频最新网址 | 一级做a视频 | 免费三级网 | 日韩免费在线观看视频 | 国产福利一区二区三区视频 | 国产五月婷婷 | 日韩精品一区二区三区免费视频观看 | 久久视频网址 | 韩国av免费观看 | 色噜噜日韩精品欧美一区二区 | 大片网站久久 | 91大神精品视频在线观看 | 天天操天天操天天操天天操天天操 | 国产精品免费视频一区二区 | 日韩亚洲欧美中文字幕 | 98久9在线 | 免费 | 在线免费观看涩涩 | 日韩视频免费在线 | 99久久综合精品五月天 | 国产真实在线 | 久久久久久久久毛片精品 | 国产精品18久久久久久vr | 国产成人一区二区三区在线观看 | 国产精品9区 | 欧美大片在线观看一区 | 天天躁天天狠天天透 | 精品欧美一区二区在线观看 | 一本一道久久a久久精品 | 欧美日韩伦理一区 | 91传媒在线看| 精品色综合| 久久综合久久八八 | 精品国内自产拍在线观看视频 | 久久精品国产一区二区电影 | 狠狠色噜噜狠狠狠 | 欧美三人交 | 日本在线精品视频 | 日韩一级片大全 | 精品福利视频在线观看 | 久久久久色 | 欧美日韩视频在线观看免费 | 玖玖在线观看视频 | 黄色成人影院 | 97在线成人| 少妇bbr搡bbb搡bbb | 久久色亚洲| 91精品视频一区二区三区 | 久久久一本精品99久久精品66 | 69久久夜色精品国产69 | 在线观看成人小视频 | 亚洲一区二区精品在线 | 亚洲欧美国产精品18p | 久草在线免费看视频 | 国产又粗又猛又色又黄视频 | 久久伊人精品一区二区三区 | 天堂av在线7| 国产精品 日韩精品 | 国产精品美女久久久久久 | 久久久久久久电影 | 亚洲精欧美一区二区精品 | 久久精彩视频 | 久草在线资源免费 | 美女禁18| 亚洲欧美成人 | 日日夜夜精品视频天天综合网 | 国产日韩欧美中文 | 久久午夜羞羞影院 | 成人免费91 | 亚洲免费av观看 | 激情欧美日韩一区二区 | 精品久久91 | 99 精品 在线 | 四虎影视成人永久免费观看视频 | 国产精品久久电影观看 | 4hu视频| 国产福利av在线 | 91麻豆精品国产午夜天堂 | 国产日韩视频在线 | 亚洲精品乱码久久久久久久久久 | 天天干天天做天天操 | 国内成人av| 久久这里有精品 | 欧美在线视频一区二区三区 | 国产美女久久 | 精品国产电影一区二区 | 91在线一区二区 | 亚洲精品色婷婷 | 国产视频不卡一区 | 黄色网在线免费观看 | 亚洲 欧美 国产 va在线影院 | 久久怡红院 | 激情丁香婷婷 | 日韩一级电影网站 | 狠狠搞,com| 黄色资源在线观看 | 久久国内精品99久久6app | 日韩欧美一区二区三区视频 | 超碰97人人爱 | 99在线热播精品免费 | 免费看av片网站 | 中文一区在线 | 最新国产精品拍自在线播放 | 欧亚日韩精品一区二区在线 | 国产中文在线播放 | 日日日操操| 激情五月六月婷婷 | 欧美日韩精品在线播放 | 日本激情视频中文字幕 | 亚洲午夜精品在线观看 | 一区免费观看 | 麻豆mv在线观看 | 亚洲va欧美va | 江苏妇搡bbbb搡bbbb | 五月婷婷激情综合网 | 久久欧美在线电影 | 91视频免费看片 | 93久久精品日日躁夜夜躁欧美 | 精品美女久久 | 国产乱码精品一区二区蜜臀 | 一级片免费观看视频 | 日韩在线免费播放 | 免费日韩av片 | 丝袜护士aⅴ在线白丝护士 天天综合精品 | 欧美精品色 | 日韩一级精品 | 亚洲综合视频在线 | 中文字幕视频三区 | 久久av一区二区三区亚洲 | 超碰在线97国产 | 亚洲国内精品视频 | 91亚洲精品久久久蜜桃 | 天天色天天综合网 | 亚洲激情综合网 | 国产精品嫩草69影院 | 国产成人精品一区二三区 | 色婷婷综合激情 | 久久久免费观看完整版 | 婷婷网五月天 | 99精品欧美一区二区三区 | 国产麻豆果冻传媒在线观看 | 国产一级黄色片免费看 | 亚洲第一中文字幕 | 中文字幕美女免费在线 | 日韩午夜在线播放 | 黄色在线观看免费网站 | 99r在线观看 | 在线观av | 亚洲国产日韩av | 五月天婷亚洲天综合网精品偷 | 国产一区在线看 | 午夜精品一区二区三区视频免费看 | 国产色道| 91福利视频免费观看 | 国产精品视频内 | 久爱精品在线 | 中文字幕 婷婷 | 国偷自产中文字幕亚洲手机在线 | 欧洲亚洲国产视频 | 999久久久久久久久 69av视频在线观看 | 99视频这里只有 | 91视频高清完整版 | 中文字幕在线免费观看视频 | 色婷婷视频 | 欧美另类性 | 色五月色开心色婷婷色丁香 | 91污视频在线 | 国产日产欧美在线观看 | 国产成人久久av977小说 | 国产精品自产拍在线观看桃花 | 亚洲九九精品 | 十八岁免进欧美 | 97**国产露脸精品国产 | 免费看污黄网站 | 久久精品a | 激情视频一区二区 | 91精品婷婷国产综合久久蝌蚪 | 奇米影视777四色米奇影院 | 91在线看黄 | 99av在线视频 | 偷拍精偷拍精品欧洲亚洲网站 | 国产精品成人一区二区三区吃奶 | 色妞色视频一区二区三区四区 | 国产99久久久欧美黑人 | 麻豆91在线播放 | 日韩影视在线观看 | 狠狠色丁香久久婷婷综合五月 | 国产一区二区三区四区大秀 | 久草视频在线看 | 中文字幕在线观看第二页 | 99热这里只有精品8 久久综合毛片 | 亚洲三级性片 | 亚洲精品综合一区二区 | 日日草视频 | 日韩中文字幕免费视频 | 国产青春久久久国产毛片 | 中文字幕第一页在线播放 | 日韩av在线免费播放 | 日韩午夜剧场 | 久久激情综合网 | 久久国产精品久久久久 | 日韩二区三区 | 开心激情婷婷 | 91麻豆传媒 | 午夜精品福利在线 | 精品1区2区3区 | 色就干| 涩涩伊人| 精品国产一区二区三区四区在线观看 | 超碰人人干人人 | 天天射天天射天天 | 久久久国产成人 | 在线91精品 | 97精品超碰一区二区三区 | 日韩sese | 午夜视频在线观看网站 | 国产三级久久久 | 成人久久久久久久久久 | 91精品一| 国产精品久久久久久久久久久久午夜 | av蜜桃在线 | av一级久久 | 99在线视频播放 | 在线视频福利 | 欧美大片mv免费 | 亚洲视频99 | 国产精品99久久久久久有的能看 | 一区二区电影网 | 国产午夜三级一区二区三桃花影视 | 在线观看中文字幕网站 | avav片 | 亚洲九九九在线观看 | 亚洲夜夜爽| 国产黄色精品在线 | www.777奇米| 国产视频欧美视频 | 国产在线播放一区二区 | 中文字幕专区高清在线观看 | 在线成人免费电影 | 免费高清看电视网站 | 亚洲一级黄色大片 | 日韩在线免费观看视频 | 国产一区免费看 | 操操操日日 | 91精品久久久久久久久 | 亚洲区另类春色综合小说校园片 | 亚洲激精日韩激精欧美精品 | 亚洲欧美视频一区二区三区 | 国产白浆视频 | 国产97免费 | 欧美日韩成人 | 麻豆成人精品视频 | 中文字幕国产精品 | 成人h在线播放 | 在线激情网| av黄色av| 久久a久久 | 国产精品porn| 一区 二区电影免费在线观看 | 在线91观看 | 99久久久国产免费 | 亚洲精品视频久久 | 国产高清一| 日韩高清精品一区二区 | 99久久精品午夜一区二区小说 | 玖玖爱国产在线 | 久久精品在线免费观看 | 99久久精品久久久久久动态片 | 日韩高清成人在线 | 4438全国亚洲精品在线观看视频 | 国产在线最新 | 园产精品久久久久久久7电影 | 黄色a在线 | 久草在线资源网 | 97夜夜澡人人双人人人喊 | www.夜夜夜 | 国产又粗又长又硬免费视频 | 91视频免费观看 | 免费亚洲黄色 | 精品伦理一区二区三区 | 日批视频| 一级欧美黄 | 国产一二区免费视频 | 免费无遮挡动漫网站 | 中文字幕在线观看免费观看 | 国产高清免费在线观看 | 久日精品 | 日日干夜夜草 | 欧美日韩一区二区在线 | 成人av在线一区二区 | 高清不卡毛片 | 免费看国产一级片 | 在线午夜| 欧美日韩精品免费观看视频 | 久久在线免费视频 | 国产又粗又猛又黄又爽视频 | 国产精品二区三区 | 国产一在线精品一区在线观看 | 一区二区 不卡 | 成人av午夜 | 九九爱免费视频在线观看 | 激情文学综合丁香 | 美女免费视频一区二区 | 欧美va日韩va | 日韩av一区二区在线播放 | 国产高清绿奴videos | 中文字幕.av.在线 | 欧美污污视频 | 激情五月在线视频 | 国产黄大片 | 欧美最猛性xxx | 超碰在线天天 | 国产高清精品在线观看 | 亚洲国产午夜视频 | 国产日产精品久久久久快鸭 | 国产一级片播放 | 日韩成人精品一区二区三区 | 最新免费av在线 | 久久精视频 | 久久精品网址 | 丰满少妇麻豆av | 亚洲第一色 | 欧美亚洲国产精品久久高清浪潮 | 国产日韩视频在线 | 中文字幕在线观看完整版 | 天天搞天天干 | 激情欧美一区二区三区 | av综合 日韩| 日韩av男人的天堂 | 精品女同一区二区三区在线观看 | 99在线精品免费视频九九视 | 在线观看黄网站 | 欧美午夜精品久久久久久浪潮 | 国产中文字幕视频在线观看 | 啪啪免费观看网站 | 在线观看国产成人av片 | 一区二区三区国产欧美 | 四虎在线观看 | 久久国产精品色av免费看 | 国产91在线 | 美洲 | 免费久久久久久 | 国产精品大片在线观看 | 亚洲不卡在线 | 日韩在线免费电影 | 免费av网站在线看 | 视频一区二区精品 | 可以免费看av | 欧美性大战久久久久 | 亚洲午夜精品一区 | 日本黄色免费播放 | 欧美另类69| 在线看国产日韩 | 美女视频免费一区二区 | 中文在线字幕观看电影 | 国产成人一区二区在线观看 | 在线免费中文字幕 | 婷婷久久网站 | 国产成人福利在线 | 91免费观看 | 欧美黄在线 | av视屏在线播放 | 国产日韩亚洲 | 久久五月天综合 | 97在线公开视频 | 99热这里只有精品在线观看 | 99视频在线精品国自产拍免费观看 | 最新中文字幕视频 | 国产一级片观看 | 亚洲国产日韩在线 | 8x8x在线观看视频 | 国产资源在线视频 | 黄色毛片视频免费观看中文 | 亚洲热久久 | 亚洲桃花综合 | 亚洲成av人片一区二区梦乃 | 91在线资源| 99精品视频免费全部在线 | 91传媒在线播放 | 丁香午夜婷婷 | av黄色亚洲 | 97精品一区二区三区 | 麻豆免费在线播放 | 在线不卡的av| 中文字幕在线观看一区二区 | 成人在线免费视频 | 91精品国产自产在线观看 | 91精品国产一区二区三区 | 国产亚洲aⅴaaaaaa毛片 | 亚洲第一中文字幕 | 久久久久一区 | 精品一区中文字幕 | 国产玖玖精品视频 | 99爱视频在线观看 | 亚洲欧洲精品久久 | 麻豆91网站 | 日日操天天操狠狠操 | 久久精品永久免费 | 天天射综合网站 | 97视频在线免费播放 | 99热.com | 日本字幕网 | 日本性视频 | 天天躁日日躁狠狠躁 | 精品久久一区二区 | 日韩精品电影在线播放 | 午夜 久久 tv | 美女视频a美女大全免费下载蜜臀 | 国产一区在线视频播放 | av电影一区| 国产欧美日韩精品一区二区免费 | 久久8| 日韩欧美在线一区二区 | 啪啪小视频网站 | 18久久久久 | 久草视频免费播放 | 91精品福利在线 | 欧美久久99| 在线电影 一区 | 欧美一级免费高清 | 中文字幕高清免费日韩视频在线 | 天天射色综合 | 在线观看麻豆av | 黄色中文字幕在线 | 久久精品日本啪啪涩涩 | 午夜精品麻豆 | 亚洲日本韩国一区二区 | 国产精品美女免费 | 天堂av在线网 | 人人爽人人爽av | 国产亚洲精品久久久久5区 成人h电影在线观看 | 成人动漫一区二区三区 | 狠狠综合网 | 国产精品久久嫩一区二区免费 | 国产区在线视频 | 精品久久久久久亚洲综合网站 | 久久av在线 | 国产成人高清在线 | 久草视频在线资源 | 久久久久久久久久久免费视频 | 92精品国产成人观看免费 | 一级片免费观看 | 欧美电影黄色 | 免费观看视频的网站 | 国产黄免费看 | 黄免费在线观看 | 日韩av一区二区在线播放 | 国产一区免费 | 久久毛片高清国产 | 国产精品 亚洲精品 | 天天曰天天曰 | 黄网av在线| 中文字幕在线看视频 | h视频日本 | 毛片精品免费在线观看 | 日日精品 | av电影中文 | 精品人人人人 | 伊人影院在线观看 | 亚洲成人精品av | 国产成人一区二区三区免费看 | 超碰97在线资源站 | 天天操夜夜拍 | 国产盗摄精品一区二区 | 色天天| 九九在线播放 | 一本—道久久a久久精品蜜桃 | 久久久麻豆 | 国产成人精品一区二区三区 | 亚洲无吗天堂 | 国产成人一区二区三区 | 国产一区二区久久精品 | 久久久久高清毛片一级 | 亚洲精品午夜久久久久久久 | 国产在线自 | 狠日日| 91九色精品女同系列 | 在线免费黄网站 | 日韩精品免费专区 | 免费看麻豆 | av黄色在线| 在线色网站 | 国产精品久久久久久麻豆一区 | 色婷婷视频在线 | 成年人免费在线观看网站 | 久久a v视频 | 成人亚洲精品国产www | 久久九九精品 | 天天色棕合合合合合合 | 免费在线激情电影 | 日本视频不卡 | 国产女人免费看a级丨片 | 99久久99视频| 国产一区观看 | 五月婷婷视频在线 | 免费精品在线观看 | 操操操人人人 | 色综合久久久久综合体 | 99久久久久久久久久 | 欧美在线一级片 | 久久热首页| 日本精品视频在线观看 | 亚洲精品视频在线免费播放 | 午夜久久久久久久久久影院 | 亚洲成人av在线播放 | 久久久久久久久久久网站 | 国产特级毛片 | 久久久久国产精品一区 | 中文字幕在线播放一区二区 | 人人爽人人澡人人添人人人人 | 亚洲精品中文字幕在线观看 | 欧美一级黄色网 | 欧美乱码精品一区二区 | 国产精品免费视频一区二区 | a视频在线 | 久久免费成人网 | 久草免费福利在线观看 | 麻豆免费看片 | 免费亚洲婷婷 | 亚洲美女在线一区 | 欧美91精品久久久久国产性生爱 | www.精选视频.com | 99精品欧美一区二区三区 | 国产精品久久一区二区无卡 | 日韩丝袜视频 | 国产中文自拍 | 99免费在线视频观看 | 蜜桃av综合网 | 欧美成人在线免费观看 | 波多野结衣动态图 | 国产成人精品999 | 国产精品美女久久久久久网站 | 免费三及片 | 久久人人爽人人 | 国产一级一级国产 | 日本中文字幕久久 | 国产精品video| 天天爱天天色 | 日韩毛片久久久 | 久久影视一区二区 | 国产精品嫩草69影院 | 精品伊人久久久 | 国产成人一级 | 胖bbbb搡bbbb擦bbbb| 久久99国产精品久久99 | 久久国产精品区 | 中文字幕在线看视频 | 亚洲热久久 | 国产精品久久电影观看 | 亚洲精品成人在线 | 色五月激情五月 | 蜜臀一区二区三区精品免费视频 | 国产专区在线视频 | 色综合 久久精品 | 色午夜影院 | 国产亚洲精品久久久网站好莱 | 香蕉手机在线 | 久久久高清免费视频 | 日韩高清免费无专码区 | 久久精品91久久久久久再现 | 玖玖视频| 黄污视频大全 | 日本亚洲国产 | 五月婷婷香蕉 | 色综合中文字幕 | 日韩在线播放av | 天天爱天天 | 日韩欧美精品在线视频 | 国产精在线 | 国产专区在线播放 | 最近在线中文字幕 | 亚洲人成人在线 | 国产99久久久国产精品免费看 | 日本精a在线观看 | 久久免费视频观看 | 日韩在线中文字幕 | 在线国产激情视频 | 美女激情影院 | 97精品超碰一区二区三区 | 五月激情亚洲 | 91免费观看视频在线 | 四虎最新入口 | 丝袜制服综合网 | 国产亚洲精品久久久久久大师 | 日韩r级电影在线观看 | 夜夜视频欧洲 | 91私密保健 | 精品久久久久久综合 | 最近中文字幕完整高清 | 伊人日日干 | 韩国精品在线观看 | 福利电影久久 | 久久久精品99 | 亚洲国产精品va在线看 | 91成人在线观看高潮 | 中午字幕在线 | 国产精品久久久久一区二区三区 | 国产精品入口a级 | 91麻豆精品一区二区三区 | 色www精品视频在线观看 | 国产区精品在线观看 | 国产裸体无遮挡 | 999久久精品 | 丁香伊人网 | 天天久久夜夜 | 国产成人精品亚洲 | 97人人模人人爽人人喊网 | 成人在线观看网址 | 欧美日韩精品国产 | 人人爽人人爽人人爽学生一级 | 在线看片91 | 色五月色开心色婷婷色丁香 | 欧美久久久久久久久久久 | 五月婷婷视频在线观看 | 精品一区二区在线观看 | 国产一区二区视频在线播放 | 奇米网777 | 色综合天天色综合 | 一区二区精品在线观看 | 天天插日日射 | 亚洲精品乱码久久 | 视频一区二区视频 | 中文字幕一区二区三区乱码在线 | 国产日韩精品在线观看 | 91九色蝌蚪视频在线 | 免费在线观看污网站 | 免费观看成人av | 久草免费在线观看 | wwwwww色 | avsex| 欧美一级视频在线观看 | 国产精品福利在线观看 | 黄污污网站 | 久久99热这里只有精品国产 | www久久国产| 高清av中文字幕 | 亚洲国产美女久久久久 | 国产精品18久久久久久久 | 91久久久久久久一区二区 | 亚洲国产日韩一区 | 国产免费一区二区三区最新6 | 久久在线影院 | 日日碰狠狠躁久久躁综合网 | 精品久久中文 | 激情xxxx| 一区 二区 精品 | 精品伦理一区二区三区 | 久久黄色影视 | 国产高清不卡在线 | 99在线国产| 日韩精品最新在线观看 | 精品国产精品久久 | 91热精品 | 96精品高清视频在线观看软件特色 | 亚洲在线免费视频 | 欧美va在线观看 | 国产黄色高清 | 色干综合| 国产视频日本 | 特级西西人体444是什么意思 | 国产亚洲精品免费 | 欧美aaaxxxx做受视频 | 日韩高清在线一区 | 中文字幕在线视频一区二区 | 在线观看免费视频你懂的 | 中文字幕久久久精品 | 91看片一区二区三区 | 欧美成人亚洲成人 | 狠狠干成人| 这里只有精品视频在线 | 91激情| 黄色aa久久 |