Electron 颜色拾取器开发实战适配鸿蒙

目录


功能概述

颜色拾取器(数码测色计)是一个实时屏幕颜色检测工具,能够精确获取屏幕上任意位置的颜色值。该工具广泛应用于设计、开发、调试等场景。

核心功能

  1. 实时颜色拾取

    • 实时跟踪鼠标位置
    • 精确获取像素颜色值
    • 支持 RGB 和 HEX 格式显示
  2. 放大镜功能

    • 实时放大显示鼠标周围区域
    • 可调节放大倍数(5x-30x)
    • 像素级精确显示
  3. 颜色信息展示

    • RGB 值显示
    • HEX 颜色码
    • 颜色样本预览
    • 鼠标位置坐标
  4. 高性能优化

    • 60fps 流畅更新
    • 智能缓存机制
    • 低延迟响应

应用场景

  • 🎨 设计工作:快速获取设计稿中的颜色值
  • 💻 前端开发:提取网页元素的颜色代码
  • 🐛 调试工具:检查界面颜色是否符合设计规范
  • 📊 数据分析:分析图片或界面的颜色分布

技术架构

系统架构图

┌─────────────────────────────────────────┐
│         主进程 (main.js)                 │
│  ┌───────────────────────────────────┐  │
│  │  屏幕截图获取 (desktopCapturer)   │  │
│  │  鼠标位置跟踪 (screen API)        │  │
│  │  截图缓存机制                     │  │
│  └───────────────────────────────────┘  │
│              ↓ IPC 通信                  │
└─────────────────────────────────────────┘
              ↓
┌─────────────────────────────────────────┐
│      渲染进程 (color-picker.html)        │
│  ┌───────────────────────────────────┐  │
│  │  Canvas 颜色提取                  │  │
│  │  放大镜渲染                       │  │
│  │  UI 更新                          │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

数据流程

1. 主进程定时获取屏幕截图(16ms间隔)
   ↓
2. 获取鼠标当前位置
   ↓
3. 通过 IPC 发送截图和鼠标位置到渲染进程
   ↓
4. 渲染进程使用 Canvas 提取颜色
   ↓
5. 更新放大镜显示和颜色信息

关键技术栈

  • Electron APIdesktopCapturerscreenipcMainipcRenderer
  • Canvas API:图像处理、像素提取、图像绘制
  • 性能优化:缓存机制、节流防抖、requestAnimationFrame

屏幕截图技术

desktopCapturer API

desktopCapturer 是 Electron 提供的屏幕捕获 API,可以获取屏幕、窗口或应用的缩略图。

const { desktopCapturer } = require('electron')

// 获取屏幕截图
const sources = await desktopCapturer.getSources({
  types: ['screen'],  // 类型:screen, window, webview
  thumbnailSize: { width: 1920, height: 1080 }  // 缩略图尺寸
})

if (sources.length > 0) {
  const screenshot = sources[0].thumbnail.toDataURL()
  // screenshot 是 base64 编码的图片数据
}

截图参数说明

types:指定要捕获的内容类型

  • 'screen':整个屏幕
  • 'window':应用窗口
  • 'webview':WebView 内容

thumbnailSize:缩略图尺寸

  • 尺寸越大,质量越高,但性能开销也越大
  • 建议使用实际屏幕分辨率

获取屏幕信息

const { screen } = require('electron')

// 获取主显示器信息
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.size
const scaleFactor = primaryDisplay.scaleFactor  // 缩放因子

// 获取所有显示器
const displays = screen.getAllDisplays()

// 获取鼠标位置
const point = screen.getCursorScreenPoint()
console.log(`鼠标位置: (${point.x}, ${point.y})`)

截图缓存优化

频繁获取屏幕截图会消耗大量资源,使用缓存可以显著提升性能:

let lastScreenshot = null
let lastScreenshotTime = 0
const SCREENSHOT_CACHE_TIME = 16 // 缓存16ms

async function getScreenshot() {
  const now = Date.now()
  
  // 如果缓存有效,直接返回
  if (lastScreenshot && (now - lastScreenshotTime) < SCREENSHOT_CACHE_TIME) {
    return lastScreenshot
  }
  
  // 获取新截图
  const sources = await desktopCapturer.getSources({
    types: ['screen'],
    thumbnailSize: primaryDisplay.size
  })
  
  if (sources.length > 0) {
    const screenshot = sources[0].thumbnail.toDataURL()
    lastScreenshot = screenshot
    lastScreenshotTime = now
    return screenshot
  }
  
  return lastScreenshot || null
}

缓存策略

  • 缓存时间:16ms(约 60fps)
  • 缓存失效:时间超过缓存时间或手动清空
  • 内存管理:避免长时间缓存大量截图

颜色提取算法

Canvas 像素提取

使用 Canvas API 从截图中提取像素颜色:

function extractColorFromScreenshot(screenshotDataUrl, x, y) {
  return new Promise((resolve) => {
    const img = new Image()
    img.onload = () => {
      // 创建临时 Canvas
      const canvas = document.createElement('canvas')
      const ctx = canvas.getContext('2d')
      
      // 设置画布尺寸
      canvas.width = img.width
      canvas.height = img.height
      
      // 绘制图片到画布
      ctx.drawImage(img, 0, 0)
      
      // 获取指定位置的像素数据
      const imageData = ctx.getImageData(x, y, 1, 1)
      const pixel = imageData.data
      
      // 返回 RGB 值
      resolve({
        r: pixel[0],  // Red (0-255)
        g: pixel[1],  // Green (0-255)
        b: pixel[2],  // Blue (0-255)
        a: pixel[3]   // Alpha (0-255)
      })
    }
    img.src = screenshotDataUrl
  })
}

ImageData 数据结构

getImageData() 返回的 ImageData 对象包含像素数据:

const imageData = ctx.getImageData(x, y, width, height)
// imageData.data 是 Uint8ClampedArray
// 每个像素占 4 个字节:R, G, B, A

// 访问像素 (x, y) 的颜色
const index = (y * width + x) * 4
const r = imageData.data[index]
const g = imageData.data[index + 1]
const b = imageData.data[index + 2]
const a = imageData.data[index + 3]

颜色格式转换

RGB 转 HEX
function rgbToHex(r, g, b) {
  return `#${[r, g, b]
    .map(x => x.toString(16).padStart(2, '0'))
    .join('')
    .toUpperCase()}`
}

// 使用示例
const hex = rgbToHex(255, 128, 64)  // "#FF8040"
HEX 转 RGB
function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null
}

// 使用示例
const rgb = hexToRgb('#FF8040')  // { r: 255, g: 128, b: 64 }
RGB 转 HSL
function rgbToHsl(r, g, b) {
  r /= 255
  g /= 255
  b /= 255
  
  const max = Math.max(r, g, b)
  const min = Math.min(r, g, b)
  let h, s, l = (max + min) / 2
  
  if (max === min) {
    h = s = 0  // 无色彩
  } else {
    const d = max - min
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
    
    switch (max) {
      case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break
      case g: h = ((b - r) / d + 2) / 6; break
      case b: h = ((r - g) / d + 4) / 6; break
    }
  }
  
  return {
    h: Math.round(h * 360),
    s: Math.round(s * 100),
    l: Math.round(l * 100)
  }
}

图片对象缓存

避免重复加载相同的截图:

let screenshotCache = null
let screenshotImageObj = null

function extractColorFromScreenshot(screenshotDataUrl, x, y) {
  return new Promise((resolve) => {
    // 如果截图没有变化,使用缓存的图片对象
    if (screenshotCache === screenshotDataUrl && screenshotImageObj) {
      extractColorFromImage(screenshotImageObj, x, y).then(resolve)
      return
    }
    
    // 新的截图,需要加载
    const img = new Image()
    img.onload = () => {
      screenshotImageObj = img
      screenshotCache = screenshotDataUrl
      extractColorFromImage(img, x, y).then(resolve)
    }
    img.src = screenshotDataUrl
  })
}

实时鼠标跟踪

获取鼠标位置

const { screen } = require('electron')

// 获取当前鼠标位置(全局坐标)
const point = screen.getCursorScreenPoint()
console.log(`鼠标位置: (${point.x}, ${point.y})`)

定时跟踪

使用 setInterval 定时获取鼠标位置:

let trackingInterval = null

function startTracking() {
  trackingInterval = setInterval(() => {
    const point = screen.getCursorScreenPoint()
    // 处理鼠标位置
    handleMouseMove(point.x, point.y)
  }, 16)  // 每16ms更新一次(约60fps)
}

function stopTracking() {
  if (trackingInterval) {
    clearInterval(trackingInterval)
    trackingInterval = null
  }
}

更新频率优化

帧率选择

  • 30fps (33ms):适合一般应用,性能开销小
  • 60fps (16ms):流畅体验,推荐用于颜色拾取器
  • 120fps (8ms):极高流畅度,但 CPU 占用高
// 60fps 配置
const UPDATE_INTERVAL = 16  // 16ms = 1000ms / 60fps

colorPickingInterval = setInterval(async () => {
  const point = screen.getCursorScreenPoint()
  const screenshot = await getScreenshot()
  // 发送数据到渲染进程
}, UPDATE_INTERVAL)

IPC 通信

主进程发送鼠标位置和截图到渲染进程:

// 主进程
ipcMain.on('start-color-picking', () => {
  setInterval(async () => {
    const point = screen.getCursorScreenPoint()
    const screenshot = await getScreenshot()
    
    colorPickerWindow.webContents.send('color-data', {
      x: point.x,
      y: point.y,
      screenshot: screenshot,
      timestamp: Date.now()
    })
  }, 16)
})

// 渲染进程
ipcRenderer.on('color-data', (event, data) => {
  const { x, y, screenshot } = data
  updateColorDisplay(x, y, screenshot)
})

Canvas API 应用

放大镜实现

使用 Canvas 绘制放大后的屏幕区域:

function drawMagnifier(img, mouseX, mouseY, scale) {
  const canvas = document.getElementById('magnifierCanvas')
  const ctx = canvas.getContext('2d')
  
  // 计算放大区域
  const sourceSize = canvas.width / scale
  const sourceX = mouseX - sourceSize / 2
  const sourceY = mouseY - sourceSize / 2
  
  // 关闭图像平滑,实现像素化效果
  ctx.imageSmoothingEnabled = false
  
  // 绘制放大后的图像
  ctx.drawImage(
    img,
    sourceX, sourceY, sourceSize, sourceSize,  // 源区域
    0, 0, canvas.width, canvas.height          // 目标区域
  )
  
  // 绘制十字准线
  drawCrosshair(ctx, canvas.width, canvas.height)
}

function drawCrosshair(ctx, width, height) {
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.8)'
  ctx.lineWidth = 1
  
  // 垂直线
  ctx.beginPath()
  ctx.moveTo(width / 2, 0)
  ctx.lineTo(width / 2, height)
  ctx.stroke()
  
  // 水平线
  ctx.beginPath()
  ctx.moveTo(0, height / 2)
  ctx.lineTo(width, height / 2)
  ctx.stroke()
  
  // 中心圆圈
  ctx.beginPath()
  ctx.arc(width / 2, height / 2, 10, 0, Math.PI * 2)
  ctx.stroke()
}

图像平滑控制

// 关闭图像平滑,实现像素化效果
ctx.imageSmoothingEnabled = false

// 或者使用更精细的控制
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'  // 'low', 'medium', 'high'

性能优化技巧

1. 使用离屏 Canvas
// 创建离屏 Canvas 用于预处理
const offscreenCanvas = document.createElement('canvas')
const offscreenCtx = offscreenCanvas.getContext('2d')

// 在离屏 Canvas 上绘制
offscreenCtx.drawImage(img, 0, 0)

// 将离屏 Canvas 内容复制到主 Canvas
ctx.drawImage(offscreenCanvas, 0, 0)
2. 批量像素操作
// 一次性获取多个像素
const imageData = ctx.getImageData(x, y, width, height)

// 批量处理像素
for (let i = 0; i < imageData.data.length; i += 4) {
  const r = imageData.data[i]
  const g = imageData.data[i + 1]
  const b = imageData.data[i + 2]
  // 处理像素...
}

// 一次性写回
ctx.putImageData(imageData, x, y)
3. 使用 requestAnimationFrame
let animationFrameId = null

function updateDisplay() {
  // 取消之前的动画帧
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
  }
  
  // 使用 requestAnimationFrame 优化渲染
  animationFrameId = requestAnimationFrame(() => {
    // 渲染逻辑
    drawMagnifier(img, mouseX, mouseY, scale)
  })
}

性能优化策略

1. 截图缓存

let lastScreenshot = null
let lastScreenshotTime = 0
const CACHE_TIME = 16  // 16ms

async function getScreenshot() {
  const now = Date.now()
  
  // 缓存有效,直接返回
  if (lastScreenshot && (now - lastScreenshotTime) < CACHE_TIME) {
    return lastScreenshot
  }
  
  // 获取新截图
  const screenshot = await captureScreen()
  lastScreenshot = screenshot
  lastScreenshotTime = now
  
  return screenshot
}

2. 图片对象复用

let screenshotImageObj = null
let screenshotCache = null

function loadScreenshot(dataUrl) {
  // 如果截图相同,复用图片对象
  if (screenshotCache === dataUrl && screenshotImageObj) {
    return Promise.resolve(screenshotImageObj)
  }
  
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => {
      screenshotImageObj = img
      screenshotCache = dataUrl
      resolve(img)
    }
    img.onerror = reject
    img.src = dataUrl
  })
}

3. 节流和防抖

// 节流:限制更新频率
let lastUpdateTime = 0
const UPDATE_THROTTLE = 16

function updateColor(data) {
  const now = performance.now()
  
  if (now - lastUpdateTime >= UPDATE_THROTTLE) {
    lastUpdateTime = now
    processUpdate(data)
  } else {
    // 保存最新数据,等待下次更新
    pendingUpdate = data
  }
}

// 防抖:延迟执行
let debounceTimer = null

function onApertureChange(value) {
  clearTimeout(debounceTimer)
  
  debounceTimer = setTimeout(() => {
    updateMagnifier(value)
  }, 50)
}

4. 待处理队列

let isProcessing = false
let pendingUpdate = null

async function updateMagnifier(data) {
  // 如果正在处理,保存最新请求
  if (isProcessing) {
    pendingUpdate = data
    return
  }
  
  isProcessing = true
  
  try {
    await processUpdate(data)
  } finally {
    isProcessing = false
    
    // 处理待处理的更新
    if (pendingUpdate) {
      const next = pendingUpdate
      pendingUpdate = null
      updateMagnifier(next)
    }
  }
}

5. 内存管理

function cleanup() {
  // 清空缓存
  lastScreenshot = null
  screenshotImageObj = null
  screenshotCache = null
  
  // 取消动画帧
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
    animationFrameId = null
  }
  
  // 清空 Canvas
  ctx.clearRect(0, 0, canvas.width, canvas.height)
}

完整代码实现

主进程代码 (main.js)

const { app, BrowserWindow, ipcMain, screen, desktopCapturer } = require('electron')

let colorPickerWindow = null
let isColorPicking = false
let colorPickingInterval = null
let lastScreenshot = null
let lastScreenshotTime = 0
const SCREENSHOT_CACHE_TIME = 16

// 创建颜色拾取器窗口
function createColorPickerWindow() {
  if (colorPickerWindow) {
    colorPickerWindow.focus()
    return
  }
  
  colorPickerWindow = new BrowserWindow({
    width: 900,
    height: 600,
    title: '数码测色计',
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  })
  
  colorPickerWindow.loadFile('color-picker.html')
  
  colorPickerWindow.on('closed', () => {
    colorPickerWindow = null
    if (isColorPicking) {
      stopColorPicking()
    }
  })
}

// 获取屏幕截图(带缓存)
async function getScreenshot() {
  const now = Date.now()
  
  if (lastScreenshot && (now - lastScreenshotTime) < SCREENSHOT_CACHE_TIME) {
    return lastScreenshot
  }
  
  try {
    const primaryDisplay = screen.getPrimaryDisplay()
    const { width, height } = primaryDisplay.size
    
    const sources = await desktopCapturer.getSources({
      types: ['screen'],
      thumbnailSize: { width, height }
    })
    
    if (sources.length > 0) {
      const screenshot = sources[0].thumbnail.toDataURL()
      lastScreenshot = screenshot
      lastScreenshotTime = now
      return screenshot
    }
  } catch (error) {
    console.error('获取截图失败:', error)
  }
  
  return lastScreenshot || null
}

// 开始颜色拾取
ipcMain.on('start-color-picking', () => {
  if (isColorPicking) return
  
  isColorPicking = true
  lastScreenshot = null
  lastScreenshotTime = 0
  
  colorPickingInterval = setInterval(async () => {
    if (!isColorPicking || !colorPickerWindow) return
    
    try {
      const point = screen.getCursorScreenPoint()
      const screenshot = await getScreenshot()
      
      if (screenshot && colorPickerWindow) {
        colorPickerWindow.webContents.send('color-data', {
          x: point.x,
          y: point.y,
          screenshot: screenshot,
          timestamp: Date.now()
        })
      }
    } catch (error) {
      console.error('颜色拾取错误:', error)
    }
  }, 16)  // 60fps
})

// 停止颜色拾取
ipcMain.on('stop-color-picking', () => {
  stopColorPicking()
})

function stopColorPicking() {
  isColorPicking = false
  if (colorPickingInterval) {
    clearInterval(colorPickingInterval)
    colorPickingInterval = null
  }
  lastScreenshot = null
  lastScreenshotTime = 0
}

// 处理打开颜色拾取器的请求
ipcMain.handle('open-color-picker', () => {
  createColorPickerWindow()
})

渲染进程代码 (color-picker.html)

const { ipcRenderer } = require('electron')

let isCapturing = false
let apertureSize = 10
let screenshotCache = null
let screenshotImageObj = null
let isProcessing = false
let pendingUpdate = null
let animationFrameId = null

const tempCanvas = document.createElement('canvas')
const tempCtx = tempCanvas.getContext('2d')
const magnifierCanvas = document.getElementById('magnifierCanvas')
const magnifierCtx = magnifierCanvas.getContext('2d')

magnifierCanvas.width = 300
magnifierCanvas.height = 300

// 从截图中提取颜色
function extractColorFromScreenshot(screenshotDataUrl, x, y) {
  return new Promise((resolve) => {
    if (screenshotCache === screenshotDataUrl && screenshotImageObj) {
      extractColorFromImage(screenshotImageObj, x, y).then(resolve)
      return
    }
    
    const img = new Image()
    img.onload = () => {
      screenshotImageObj = img
      screenshotCache = screenshotDataUrl
      extractColorFromImage(img, x, y).then(resolve)
    }
    img.onerror = () => resolve({ r: 0, g: 0, b: 0 })
    img.src = screenshotDataUrl
  })
}

function extractColorFromImage(img, x, y) {
  return new Promise((resolve) => {
    if (tempCanvas.width !== img.width || tempCanvas.height !== img.height) {
      tempCanvas.width = img.width
      tempCanvas.height = img.height
    }
    tempCtx.drawImage(img, 0, 0)
    
    const imageData = tempCtx.getImageData(x, y, 1, 1)
    const pixel = imageData.data
    
    resolve({
      r: pixel[0],
      g: pixel[1],
      b: pixel[2]
    })
  })
}

// 更新放大镜
async function updateMagnifier(data) {
  if (!data || !data.screenshot) return
  
  if (isProcessing) {
    pendingUpdate = data
    return
  }
  
  isProcessing = true
  const { screenshot, x, y } = data
  
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
  }
  
  animationFrameId = requestAnimationFrame(async () => {
    try {
      let img = screenshotImageObj
      
      if (screenshotCache !== screenshot || !img) {
        img = new Image()
        await new Promise((resolve, reject) => {
          img.onload = resolve
          img.onerror = reject
          img.src = screenshot
        })
        screenshotImageObj = img
        screenshotCache = screenshot
      }
      
      const scale = apertureSize
      const sourceSize = 300 / scale
      const sourceX = Math.max(0, x - sourceSize / 2)
      const sourceY = Math.max(0, y - sourceSize / 2)
      
      magnifierCtx.imageSmoothingEnabled = false
      magnifierCtx.drawImage(
        img,
        sourceX, sourceY, sourceSize, sourceSize,
        0, 0, 300, 300
      )
      
      const color = await extractColorFromScreenshot(screenshot, x, y)
      updateColorDisplay({ ...color, x, y })
    } catch (error) {
      console.error('更新失败:', error)
    } finally {
      isProcessing = false
      if (pendingUpdate) {
        const next = pendingUpdate
        pendingUpdate = null
        updateMagnifier(next)
      }
    }
  })
}

// 更新颜色显示
function updateColorDisplay(data) {
  const { r, g, b, x, y } = data
  
  document.getElementById('colorSwatch').style.backgroundColor = `rgb(${r}, ${g}, ${b})`
  document.getElementById('redValue').textContent = r
  document.getElementById('greenValue').textContent = g
  document.getElementById('blueValue').textContent = b
  
  const hex = `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`.toUpperCase()
  document.getElementById('hexValue').textContent = hex
  document.getElementById('pixelInfo').textContent = `位置: (${x}, ${y})`
}

// 开始拾色
function startColorPicking() {
  isCapturing = true
  ipcRenderer.send('start-color-picking')
  
  let lastUpdateTime = 0
  const UPDATE_THROTTLE = 16
  
  ipcRenderer.on('color-data', (event, data) => {
    if (!isCapturing) return
    
    const now = performance.now()
    if (now - lastUpdateTime >= UPDATE_THROTTLE) {
      lastUpdateTime = now
      updateMagnifier(data)
    } else {
      pendingUpdate = data
    }
  })
}

// 停止拾色
function stopColorPicking() {
  isCapturing = false
  ipcRenderer.send('stop-color-picking')
  
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
    animationFrameId = null
  }
  
  screenshotCache = null
  screenshotImageObj = null
  pendingUpdate = null
  isProcessing = false
  
  ipcRenderer.removeAllListeners('color-data')
}

功能扩展

1. 颜色历史记录

let colorHistory = []

function addToHistory(color) {
  colorHistory.unshift(color)
  if (colorHistory.length > 20) {
    colorHistory.pop()
  }
  updateHistoryDisplay()
}

function updateHistoryDisplay() {
  const container = document.getElementById('colorHistory')
  container.innerHTML = colorHistory.map((color, index) => {
    return `
      <div class="history-item" data-index="${index}">
        <div class="history-swatch" style="background: rgb(${color.r}, ${color.g}, ${color.b})"></div>
        <div class="history-info">
          <div>RGB: ${color.r}, ${color.g}, ${color.b}</div>
          <div>HEX: ${rgbToHex(color.r, color.g, color.b)}</div>
        </div>
      </div>
    `
  }).join('')
}

2. 颜色格式转换

function convertColorFormat(r, g, b, format) {
  switch (format) {
    case 'hex':
      return rgbToHex(r, g, b)
    case 'rgb':
      return `rgb(${r}, ${g}, ${b})`
    case 'rgba':
      return `rgba(${r}, ${g}, ${b}, 1)`
    case 'hsl':
      const hsl = rgbToHsl(r, g, b)
      return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`
    default:
      return rgbToHex(r, g, b)
  }
}

3. 颜色对比度检测

function getContrastRatio(color1, color2) {
  const l1 = getLuminance(color1.r, color1.g, color1.b)
  const l2 = getLuminance(color2.r, color2.g, color2.b)
  
  const lighter = Math.max(l1, l2)
  const darker = Math.min(l1, l2)
  
  return (lighter + 0.05) / (darker + 0.05)
}

function getLuminance(r, g, b) {
  const [rs, gs, bs] = [r, g, b].map(val => {
    val = val / 255
    return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4)
  })
  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
}

4. 颜色调色板生成

function generatePalette(baseColor) {
  const { r, g, b } = baseColor
  const palette = []
  
  // 生成不同亮度的颜色
  for (let i = 0; i < 5; i++) {
    const factor = i / 4
    palette.push({
      r: Math.round(r * factor),
      g: Math.round(g * factor),
      b: Math.round(b * factor)
    })
  }
  
  return palette
}

最佳实践

1. 权限处理

在 macOS 上,屏幕录制需要用户授权:

// 检查权限
const { systemPreferences } = require('electron')

async function checkScreenRecordingPermission() {
  if (process.platform === 'darwin') {
    const status = systemPreferences.getMediaAccessStatus('screen')
    if (status !== 'granted') {
      // 提示用户授权
      dialog.showMessageBox({
        type: 'info',
        title: '需要屏幕录制权限',
        message: '请在系统偏好设置中允许此应用进行屏幕录制'
      })
      return false
    }
  }
  return true
}

2. 错误处理

async function getScreenshot() {
  try {
    const sources = await desktopCapturer.getSources({
      types: ['screen'],
      thumbnailSize: primaryDisplay.size
    })
    
    if (sources.length === 0) {
      throw new Error('无法获取屏幕截图')
    }
    
    return sources[0].thumbnail.toDataURL()
  } catch (error) {
    console.error('截图失败:', error)
    
    // 显示用户友好的错误信息
    if (error.message.includes('permission')) {
      showPermissionError()
    }
    
    return null
  }
}

3. 资源清理

function cleanup() {
  // 停止定时器
  if (colorPickingInterval) {
    clearInterval(colorPickingInterval)
    colorPickingInterval = null
  }
  
  // 清空缓存
  lastScreenshot = null
  screenshotImageObj = null
  
  // 取消动画帧
  if (animationFrameId) {
    cancelAnimationFrame(animationFrameId)
    animationFrameId = null
  }
  
  // 移除事件监听器
  ipcRenderer.removeAllListeners('color-data')
}

4. 性能监控

let frameCount = 0
let lastFpsTime = Date.now()

function updateFPS() {
  frameCount++
  const now = Date.now()
  
  if (now - lastFpsTime >= 1000) {
    const fps = frameCount
    frameCount = 0
    lastFpsTime = now
    
    console.log(`FPS: ${fps}`)
    document.getElementById('fpsDisplay').textContent = `FPS: ${fps}`
  }
}

常见问题

1. 截图权限问题

问题:在 macOS 上无法获取屏幕截图。

解决方案

  1. 系统偏好设置 → 安全性与隐私 → 屏幕录制
  2. 勾选 Electron 应用
  3. 重启应用

2. 性能问题

问题:颜色拾取器运行卡顿。

解决方案

  • 降低更新频率(从 16ms 改为 33ms)
  • 减小截图尺寸
  • 优化缓存策略
  • 使用 Web Worker 处理颜色提取

3. 颜色不准确

问题:提取的颜色值与实际不符。

可能原因

  • 屏幕缩放因子未考虑
  • 颜色空间转换问题
  • 截图质量过低

解决方案

// 考虑屏幕缩放因子
const scaleFactor = primaryDisplay.scaleFactor
const pixelX = Math.floor(x * scaleFactor)
const pixelY = Math.floor(y * scaleFactor)

4. 内存泄漏

问题:长时间运行后内存占用增加。

解决方案

  • 定期清理缓存
  • 限制历史记录数量
  • 及时释放图片对象
  • 使用 WeakMap 存储临时数据

5. 跨平台兼容性

问题:不同平台行为不一致。

解决方案

function getPlatformSpecificConfig() {
  switch (process.platform) {
    case 'darwin':
      return {
        screenshotInterval: 16,
        cacheTime: 16
      }
    case 'win32':
      return {
        screenshotInterval: 20,
        cacheTime: 20
      }
    case 'linux':
      return {
        screenshotInterval: 33,
        cacheTime: 33
      }
    default:
      return {
        screenshotInterval: 33,
        cacheTime: 33
      }
  }
}

总结

通过本文,我们学习了:

  1. 屏幕截图技术:使用 desktopCapturer API 获取屏幕截图
  2. 颜色提取算法:使用 Canvas API 提取像素颜色
  3. 实时鼠标跟踪:使用 screen API 跟踪鼠标位置
  4. Canvas 应用:实现放大镜和图像处理
  5. 性能优化:缓存、节流、防抖等优化策略
  6. IPC 通信:主进程与渲染进程的数据传递

关键要点

  • 截图缓存可以显著减少性能开销
  • requestAnimationFrame提供流畅的渲染体验
  • 节流和防抖避免过度更新
  • 图片对象复用减少内存分配
  • 错误处理对于提升用户体验至关重要

下一步学习


祝您开发愉快! 🚀


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐