最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

一文詳解為何在Vue3中uni.createCanvasContext畫不出canvas

 更新時間:2026年01月20日 11:39:28   作者:超級無敵大蟑王  
這篇文章主要介紹了為何在Vue3中uni.createCanvasContext畫不出canvas的相關(guān)資料,包括Canvas畫布空白、組件作用域不同導(dǎo)致的渲染失敗以及如何實現(xiàn)連線效果,文中通過代碼介紹的非常詳細,需要的朋友可以參考下

1. 現(xiàn)象描述

當(dāng)我們封裝一個通用的手勢組件< GestureCheckIn/> 時,最直觀的操作就是在父組件中直接引入。

代碼看起來非常完美:

<GestureCheckIn @confirm="handleGestureSubmit" />

然而,當(dāng)我們在 UniApp (Vue 3) 環(huán)境下運行這段代碼時,經(jīng)常遇到一種“代碼沒報錯,但畫面一片白”的Bug:

  •  Canvas 畫布全是空白,無論怎么在這個區(qū)域滑動手勢,都沒有線條出現(xiàn)。

  •  控制臺沒有報錯,  draw() 方法似乎執(zhí)行了,  console.log 也能打印出坐標(biāo)點。

而當(dāng)我們逐步檢查時,初始化 Canvas 的代碼長這樣:

ctx = uni.createCanvasContext('gestureCanvas')

如果我們把這段代碼從組件里搬出來,直接寫在根頁面里,它又是正常的。這背后的根本原因,在于 組件作用域 的不同。

2. 小程序端的陷阱

當(dāng)你直接調(diào)用 時,uni.createCanvasContext('id') UniApp 默認是在 當(dāng)前頁面 的范圍內(nèi)查找這個canvas-id。

但是,現(xiàn)在我們將 Canvas 封裝在了一個 自定義組件 內(nèi)部。為了實現(xiàn)組件化隔離,Vue 3 對組件內(nèi)部的 DOM 節(jié)點進行了封裝。

限制一:為何渲染失???

這是新手在 Vue 3 + UniApp 最容易踩的坑。

如果不傳入第二個參數(shù),UniApp 會去頁面根節(jié)點找 gestureCanvas 。但因為你的 Canvas 藏在 < GestureCheckIn/> 組件的 Shadow DOM 或者組件作用域里,它根本找不到

錯誤的代碼:

// 在 Vue 3 組件中,這樣寫會導(dǎo)致找不到 Canvas 上下文
ctx = uni.createCanvasContext('gestureCanvas') 

解決方案:顯式傳入 instance

在 Vue 3 的 setup語法糖中,我們沒有 this 。我們需要手動獲取當(dāng)前組件的實例,并把它作為第二個參數(shù)傳給創(chuàng)建函數(shù),告訴 UniApp:“請在這個組件實例的范圍內(nèi)找 Canvas”。

正確的代碼(Vue 3 正解):

import { getCurrentInstance, onMounted } from 'vue'

// 獲取當(dāng)前組件實例
const instance = getCurrentInstance()
onMounted(() => {
  // 必須將 instance 傳進去!
  ctx = uni.createCanvasContext('gestureCanvas', instance)
  if (ctx) {
     draw() 
  }
})

3. 如何實現(xiàn)連線效果?

解決了畫不出來的問題,下一個挑戰(zhàn)是:如何讓線條既連接已選中的點,又能實時跟隨手指移動?

很多新手實現(xiàn)的連線效果往往是“斷裂”的,或者只能連接點與點,沒有那條“正在尋找下一個點”的動態(tài)線。

核心邏輯拆解

要實現(xiàn)完美的連線,我們需要在 draw() 函數(shù)中分兩步走:

  1. 連接“歷史”:畫出已經(jīng)確定的點之間的線段。

  2. 連接“當(dāng)下”:畫出最后一個點到手指當(dāng)前位置的線段。

關(guān)鍵代碼解析

// 繪制連線邏輯
if (selectedIndices.length > 0) {
  ctx.beginPath() // 必須開啟新路徑,否則會和圓點的繪制混在一起
  
  // 1. 移動畫筆到第一個選中的點
  const startPoint = points[selectedIndices[0]]
  ctx.moveTo(startPoint.x, startPoint.y)
  
  // 2. 遍歷后續(xù)所有已選中的點,將它們連起來
  for (let i = 1; i < selectedIndices.length; i++) {
    const p = points[selectedIndices[i]]
    ctx.lineTo(p.x, p.y)
  }
  
  // 3. 【關(guān)鍵】如果是正在觸摸狀態(tài),畫一條線到當(dāng)前手指的位置
  // 這就是為什么手勢看起來像在“拉橡皮筋”
  if (isDrawing) {
    ctx.lineTo(currentPos.x, currentPos.y)
  }
  
  // 4. 樣式設(shè)置(改為藍色主題 #007AFF)
  ctx.setStrokeStyle('#007AFF') 
  ctx.setLineWidth(6)
  
  // 5. 【細節(jié)】讓線條拐角和端點變得圓潤,防止出現(xiàn)鋸齒或尖角
  ctx.setLineCap('round') 
  ctx.setLineJoin('round')
  
  ctx.stroke()
}

視覺優(yōu)化Tips:

  • ctx.beginPath() 的重要性:在 Canvas 中,如果你不重新 beginPath,當(dāng)你調(diào)用 stroke 時,它會把之前畫過的所有圓圈再描一遍邊,導(dǎo)致性能下降且樣式混亂。

  • 動態(tài)跟隨: 當(dāng)下的點是在 touchmove 事件中實時更新的。只有將它加入到 lineTo 序列的最后,用戶才會感覺線條是“長”在手上的。

  • 圓角處理:默認的線條連接處是尖的,在手勢轉(zhuǎn)折時非常難看。設(shè)置為 round 可以讓折線連接處變得平滑圓潤,提升質(zhì)感。

4. 組件最終實現(xiàn)方案:

<template>
  <view class="gesture-container">
    <canvas canvas-id="gestureCanvas" id="gestureCanvas" class="gesture-canvas" @touchstart="start" @touchmove="move"
      @touchend="end"></canvas>
    <view class="action">
      <button @click="reset">重設(shè)手勢</button>
      <text class="debug-info">{{ debugInfo }}</text>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted, defineEmits } from 'vue'
import { getCurrentInstance } from 'vue'

// 定義顏色常量
const themeColor = '#007AFF'
const themeColorDark = '#005BBB' 

const instance = getCurrentInstance()
const emit = defineEmits(['confirm'])

let ctx = null
const debugInfo = ref('等待初始化...')

// 基礎(chǔ)樣式配置
const canvasWidth = 300
const canvasHeight = 300
const r = 25

// 狀態(tài)
let isDrawing = false
let points = []
let selectedIndices = []
let currentPos = { x: 0, y: 0 }

onMounted(() => {
  setTimeout(() => {
    ctx = uni.createCanvasContext('gestureCanvas', instance)

    if (ctx) {
      initPoints()
      draw()
    }
  }, 200)
})

const initPoints = () => {
  points = []
  const padding = 50
  const stepX = (canvasWidth - 2 * padding) / 2
  const stepY = (canvasHeight - 2 * padding) / 2

  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      points.push({
        x: padding + j * stepX,
        y: padding + i * stepY,
        index: i * 3 + j
      })
    }
  }
  debugInfo.value = '請繪制手勢密碼(至少連接4個點)'
}

const draw = () => {
  if (!ctx) {
    return
  }

  ctx.clearRect(0, 0, canvasWidth, canvasHeight)

  points.forEach((p, index) => {
    // 繪制外圓路徑
    ctx.beginPath()
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2)
    ctx.setLineWidth(3)
    ctx.setStrokeStyle(themeColor)

    if (selectedIndices.includes(index)) {
      // 選中狀態(tài):繪制外圈邊框 + 內(nèi)部實心圓點
      ctx.stroke()

      ctx.beginPath()
      ctx.arc(p.x, p.y, r / 3.5, 0, Math.PI * 2)
      // 選中時的內(nèi)部實心圓點
      ctx.setFillStyle(themeColor)
      ctx.fill()
    } else {
      // 未選中狀態(tài):白色填充 + 邊框
      ctx.setFillStyle('#ffffff')
      ctx.fill()
      ctx.stroke()
    }
  })

  // 繪制連線
  if (selectedIndices.length > 0) {
    ctx.beginPath()
    const startPoint = points[selectedIndices[0]]
    ctx.moveTo(startPoint.x, startPoint.y)
    for (let i = 1; i < selectedIndices.length; i++) {
      const p = points[selectedIndices[i]]
      ctx.lineTo(p.x, p.y)
    }
    if (isDrawing) {
      ctx.lineTo(currentPos.x, currentPos.y)
    }
    // 連線顏色改為藍色
    ctx.setStrokeStyle(themeColor)
    ctx.setLineWidth(6)
    ctx.setLineCap('round')
    ctx.setLineJoin('round')
    ctx.stroke()
  }

  ctx.draw()

  if (selectedIndices.length > 0) {
    debugInfo.value = `已連接 ${selectedIndices.length} 個點`
  }
}

const getPosition = (e) => {
  if (!e.touches || !e.touches[0]) {
    return { x: 0, y: 0 }
  }
  // 增加兼容性處理,防止部分環(huán)境 e.touches[0] 不包含 x, y
  const touch = e.touches[0]
  return {
    x: touch.x || touch.clientX,
    y: touch.y || touch.clientY
  }
}

const start = (e) => {
  isDrawing = true
  selectedIndices = []
  const pos = getPosition(e)
  currentPos = pos
  checkCollision(pos)
  draw()
}

const move = (e) => {
  if (!isDrawing) return
  const pos = getPosition(e)
  currentPos = pos
  checkCollision(pos)
  draw()
}

const end = (e) => {
  isDrawing = false
  draw()

  if (selectedIndices.length >= 4) {
    const pattern = selectedIndices.join('')
    emit('confirm', pattern)
    debugInfo.value = `手勢已確認`
  } else if (selectedIndices.length > 0) {
    debugInfo.value = `至少需要連接 4 個點(已連接 ${selectedIndices.length} 個)`
    setTimeout(() => {
      reset()
    }, 1500)
  }
}

const checkCollision = (pos) => {
  points.forEach((p, i) => {
    const dx = pos.x - p.x
    const dy = pos.y - p.y
    const distance = Math.sqrt(dx * dx + dy * dy)
    if (distance < r && !selectedIndices.includes(i)) {
      selectedIndices.push(i)
    }
  })
}

const reset = () => {
  selectedIndices = []
  isDrawing = false
  currentPos = { x: 0, y: 0 }
  draw()
  debugInfo.value = '請繪制手勢密碼(至少連接4個點)'
}
</script>

<style lang="scss" scoped>
// 定義 CSS 變量以便樣式中使用
$theme-color: #007AFF;
$theme-color-dark: #005BBB;
$theme-shadow-light: rgba(0, 122, 255, 0.1);
$theme-shadow-medium: rgba(0, 122, 255, 0.2);

.gesture-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background: #ffffff;
  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.03);
  border-radius: 24rpx;
}

.gesture-canvas {
  width: 300px;
  height: 300px;
  background: #ffffff;
  border: 2px solid $theme-color;
  border-radius: 12px;
  box-shadow: 0 2px 8px $theme-shadow-light;
}

.action {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;

  button {
    padding: 12px 30px;
    background: $theme-color;
    color: #ffffff;
    border: none;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 500;
    box-shadow: 0 2px 6px $theme-shadow-medium;

    &:active {
      background: $theme-color-dark;
    }
  }

  .debug-info {
    font-size: 13px;
    color: $theme-color;
    text-align: center;
  }
}
</style>

總結(jié)

到此這篇關(guān)于為何在Vue3中uni.createCanvasContext畫不出canvas的文章就介紹到這了,更多相關(guān)Vue3 uni.createCanvasContext畫不出canvas內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • JavaScript關(guān)于某元素點擊事件的監(jiān)聽和觸發(fā)

    JavaScript關(guān)于某元素點擊事件的監(jiān)聽和觸發(fā)

    本文主要介紹了JavaScript關(guān)于某元素點擊事件的監(jiān)聽和觸發(fā),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • javascript  Error 對象 錯誤處理

    javascript Error 對象 錯誤處理

    javascript常見error對象處理,錯誤代碼集合
    2008-05-05
  • javascript實現(xiàn)去除HTML標(biāo)簽的方法

    javascript實現(xiàn)去除HTML標(biāo)簽的方法

    這篇文章主要介紹了javascript實現(xiàn)去除HTML標(biāo)簽的方法,涉及javascript正則替換相關(guān)操作技巧,需要的朋友可以參考下
    2016-12-12
  • JS根據(jù)變量保存方法名并執(zhí)行方法示例

    JS根據(jù)變量保存方法名并執(zhí)行方法示例

    用eval方法,把傳進來的這個方法名所代表的方法當(dāng)作一個對象來賦值給method1的func屬性,需要的朋友可以參考下
    2014-04-04
  • 原生javascript實現(xiàn)連連看游戲

    原生javascript實現(xiàn)連連看游戲

    這篇文章主要為大家詳細介紹了原生javascript實現(xiàn)連連看游戲,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-01-01
  • JavaScript定義全局對象的方法示例

    JavaScript定義全局對象的方法示例

    這篇文章主要介紹了JavaScript定義全局對象的方法,結(jié)合實例形式分析了javascript全局對象的簡單定義流程與實現(xiàn)技巧,需要的朋友可以參考下
    2017-01-01
  • javascript中加var和不加var的區(qū)別 你真的懂嗎

    javascript中加var和不加var的區(qū)別 你真的懂嗎

    var 語句用于聲明變量,本文給大家介紹javascript 中加’var‘和不加'var'的區(qū)別,涉及到j(luò)avascript var相關(guān)知識,對javascript var相關(guān)知識感興趣的朋友一起學(xué)習(xí)吧
    2016-01-01
  • 詳解微信小程序的 request 封裝示例

    詳解微信小程序的 request 封裝示例

    這篇文章主要介紹了詳解微信小程序的 request 封裝示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08
  • 最新評論

    图们市| 苏尼特右旗| 沙雅县| 吉木萨尔县| 龙井市| 平湖市| 上杭县| 武平县| 宁阳县| 增城市| 石棉县| 正阳县| 黑山县| 玉屏| 富锦市| 阿克| 纳雍县| 固始县| 始兴县| 石楼县| 桐梓县| 诸城市| 井陉县| 泸州市| 青阳县| 商洛市| 浦北县| 海宁市| 新津县| 定兴县| 尉氏县| 马边| 龙泉市| 鸡泽县| 云梦县| 青浦区| 定日县| 辽阳县| 峡江县| 洪雅县| 林芝县|