DEV Community

SoulHarbor
SoulHarbor

Posted on

1

ArkTS Multi-Layer Image Rendering Class Implementation (Part I)

This article analyzes the implementation of a professional 2D rendering system for HarmonyOS using object-oriented design principles, based on actual production code. Part I focuses on core system architecture and fundamental rendering capabilities.

I. Dual-Buffer Rendering Engine Architecture

1. Off-Screen Canvas System

  constructor(layerNo: number, layerName: string, widthPX: number, heightPX: number,
    content?: ImageBitmap, viewable?: boolean, lock?: boolean) {
    this.layerNo = layerNo;
    this.layerName = layerName;
    this.canView = true;
    this.lock = false;
    this.widthPX = widthPX;
    this.heightPX = heightPX;
    this.hiddenCanvas = new OffscreenCanvas(widthPX, heightPX, LengthMetricsUnit.PX);
    this.hiddenContext = this.hiddenCanvas.getContext("2d", this.settings);
    //this.visualCanvas=new CanvasRenderingContext2D(settings)
    if (content) {
      this.hiddenContext.drawImage(content, 0, 0);
    }
    if (viewable !== null && viewable !== undefined) {
      this.canView = viewable;
    }
    if (lock !== null && lock !== undefined) {
      this.lock = this.lock;
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Dual Buffering Mechanism: Utilizes OffscreenCanvas for non-main-thread rendering
  • Anti-aliasing Control: RenderingContextSettings(false) disables anti-aliasing for pixel-perfect precision
  • Context Isolation: Each layer maintains independent rendering context for complex composition operations

2. Layer Management System

@ObservedV2
export class Layers {
  @Trace data: Layer[] = [];
}
Enter fullscreen mode Exit fullscreen mode
  • Reactive Layer Stack: Uses @ObservedV2 for automatic UI updates on layer changes
  • Layer Properties: Encapsulates canvas dimensions, visibility, and lock state
  • Rendering Order: Controlled by array sequence (later layers render on top)

II. Core Rendering Capabilities

1. Basic Rendering Operations

Brush System

  public async draw(pxX: number, pxY: number, color: string, weight: number) {
    //await this.clear(pxX, pxY, weight, true);
    this.hiddenContext.fillStyle = color;
    if ((pxX === this.lastPointX && pxY === this.lastPointY) || this.lastPointX === -1 || this.lastPointY === -1) {
      this.hiddenContext.globalCompositeOperation = 'destination-out';
      this.hiddenContext.fillStyle = '#ffffffff';
      switch (weight) {
        case 1:
          this.hiddenContext.fillRect(pxX, pxY, 1, 1)
          break
        case 2:
          this.hiddenContext.fillRect(pxX, pxY, 2, 2)
          break
        case 3:
          this.hiddenContext.fillRect(pxX - 1, pxY - 1, 3, 3)
          break
        case 4:
          this.hiddenContext.fillRect(pxX - 1, pxY, 4, 2)
          this.hiddenContext.fillRect(pxX, pxY - 1, 2, 4)
          break
        case 5:
          this.hiddenContext.fillRect(pxX - 2, pxY - 1, 5, 3)
          this.hiddenContext.fillRect(pxX - 1, pxY - 2, 3, 5)
          break
      }
      this.hiddenContext.globalCompositeOperation = 'source-over';
      this.hiddenContext.fillStyle = color;
      switch (weight) {
        case 1:
          this.hiddenContext.fillRect(pxX, pxY, 1, 1)
          break
        case 2:
          this.hiddenContext.fillRect(pxX, pxY, 2, 2)
          break
        case 3:
          this.hiddenContext.fillRect(pxX - 1, pxY - 1, 3, 3)
          break
        case 4:
          this.hiddenContext.fillRect(pxX - 1, pxY, 4, 2)
          this.hiddenContext.fillRect(pxX, pxY - 1, 2, 4)
          break
        case 5:
          this.hiddenContext.fillRect(pxX - 2, pxY - 1, 5, 3)
          this.hiddenContext.fillRect(pxX - 1, pxY - 2, 3, 5)
          break
      }
      // this.hiddenContext.beginPath();
      // this.hiddenContext.arc(pxX, pxY, weight/2, 0, 2 * Math.PI);
      // this.hiddenContext.stroke();
      // this.hiddenContext.fill();
      // this.hiddenContext.closePath();
    } else {
      this.hiddenContext.globalCompositeOperation = 'destination-out';
      this.shape(0, this.lastPointX, this.lastPointY, pxX, pxY, '#ffffffff', weight, false, false);
      this.hiddenContext.globalCompositeOperation = 'source-over';
      this.shape(0, this.lastPointX, this.lastPointY, pxX, pxY, color, weight, false, false);
    }
    this.setPoint(pxX, pxY);
  }
Enter fullscreen mode Exit fullscreen mode
  • Composite Modes: Implements transparency effects through globalCompositeOperation
  • Dynamic Brush System: Adjusts rendering area automatically based on weight (1-5 levels)
  • Path Caching: Maintains last coordinates for continuous line drawing

Vector Shape Rendering

  public shape(type: number, fromX: number, fromY: number, toX: number, toY: number, color: string, weight: number,
    fillShape?: boolean, first: boolean = true) {
    if(first){
      this.hiddenContext.globalCompositeOperation = 'destination-out';
      this.shape(type, fromX, fromY, toX, toY, color, weight, fillShape, false);
      this.hiddenContext.globalCompositeOperation = 'source-over';
    }
    this.hiddenContext.beginPath()
    switch (type) {
      case 0:
        this.hiddenContext.moveTo(fromX, fromY)
        this.hiddenContext.lineTo(toX, toY)
        break
      case 1:
        this.hiddenContext.rect(fromX, fromY, toX - fromX, toY - fromY)
        break
      case 2: {
        let radius = Math.sqrt(Math.pow(toX - fromX, 2) + Math.pow(toY - fromY, 2)) / 2
        let centerX = (fromX + toX) / 2
        let centerY = (fromY + toY) / 2
        this.hiddenContext.arc(centerX, centerY, radius, 0, 2 * Math.PI, false)
        break
      }
      case 3: {
        let x = (fromX + toX) / 2
        let y = (fromY + toY) / 2
        let w = Math.abs(toX - fromX) / 2
        let h = Math.abs(toY - fromY) / 2
        this.hiddenContext.ellipse(x, y, w, h, 0, 0, 2.1 * Math.PI)
        break
      }
    }
    this.hiddenContext.lineWidth = weight
    this.hiddenContext.strokeStyle = color
    this.hiddenContext.stroke()
    if (!(type === 0) && fillShape) {
      this.hiddenContext.fillStyle = color
      this.hiddenContext.fill()
    }
    this.hiddenContext.closePath()
  }
Enter fullscreen mode Exit fullscreen mode
  • Shape Variety: Supports lines, rectangles, circles, and ellipses
  • Fill Control: Boolean parameter controls shape filling
  • Style Management: Centralized control through strokeStyle configuration

2. Advanced Rendering Features

Flood Fill Algorithm

  public async splash(pxX: number, pxY: number, color: string, maxDepth = 32, first: boolean = true) {
    //promptAction.showToast({message:CJNative().hello_cangjie('FUCK')})
    if (first) {
      this.hiddenContext.globalCompositeOperation = 'destination-out';
      this.splash(pxX, pxY, '#ffffffff', 32, false);
      this.hiddenContext.globalCompositeOperation = 'source-over';
    }
    const targetColor = this.hiddenContext.getImageData(pxX, pxY, 1, 1).data;
    const rgb = __XColorData__.autoStr2rgba(color);
    let pixelsToFill: [number, number, number][] = [[pxX, pxY, 0]];
    const checkedPixels = new Set<string>();
    while (pixelsToFill.length > 0) {
      let currentPixel = pixelsToFill.shift();
      if (!currentPixel) {
        continue;
      }
      let currentX = currentPixel[0];
      let currentY = currentPixel[1];
      let currentDepth = currentPixel[2];
      const key = `${currentX},${currentY}`;
      if (checkedPixels.has(key) || currentDepth >= maxDepth) {
        continue;
      }
      checkedPixels.add(key);
      const currentColor = this.hiddenContext.getImageData(currentX, currentY, 1, 1).data;
      if (currentColor[0] === targetColor[0] &&
        currentColor[1] === targetColor[1] &&
        currentColor[2] === targetColor[2] &&
        currentColor[3] === targetColor[3]) {
        if (currentX > 0) {
          pixelsToFill.push([currentX - 1, currentY, currentDepth + 1]);
        } // Left
        if (currentX < this.widthPX - 1) {
          pixelsToFill.push([currentX + 1, currentY, currentDepth + 1]);
        } // Right
        if (currentY > 0) {
          pixelsToFill.push([currentX, currentY - 1, currentDepth + 1]);
        } // Top
        if (currentY < this.heightPX - 1) {
          pixelsToFill.push([currentX, currentY + 1, currentDepth + 1]);
        } // Bottom
        this.hiddenContext.fillStyle = color;
        this.hiddenContext.fillRect(currentX, currentY, 1, 1);
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Color Matching: Retrieves target pixel color via getImageData
  • Depth Limiting: Prevents infinite recursion with maxDepth parameter
  • Performance Optimization: Uses Breadth-First Search (BFS) for efficient filling

Eraser Functionality

  public async clear(pxX: number, pxY: number, weight: number, autoBeforeDrawing: boolean = false) {
    this.hiddenContext.globalCompositeOperation = 'destination-out';
    if ((pxX === this.lastPointX && pxY === this.lastPointY) || this.lastPointX === -1 || this.lastPointY === -1) {
      switch (weight) {
        case 1:
          this.hiddenContext.fillRect(pxX, pxY, 1, 1)
          break
        case 2:
          this.hiddenContext.fillRect(pxX, pxY, 2, 2)
          break
        case 3:
          this.hiddenContext.fillRect(pxX - 1, pxY - 1, 3, 3)
          break
        case 4:
          this.hiddenContext.fillRect(pxX - 1, pxY, 4, 2)
          this.hiddenContext.fillRect(pxX, pxY - 1, 2, 4)
          break
        case 5:
          this.hiddenContext.fillRect(pxX - 2, pxY - 1, 5, 3)
          this.hiddenContext.fillRect(pxX - 1, pxY - 2, 3, 5)
          break
      }
    } else {
      this.shape(0, this.lastPointX, this.lastPointY, pxX, pxY, '#ffffffff', weight, false, false);
    }
    this.hiddenContext.globalCompositeOperation = 'source-over';
    if (!autoBeforeDrawing) {
      this.setPoint(pxX, pxY);
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Precision Erasing: Selective region clearing through composite modes
  • Weight Adaptation: Automatically adjusts erasing area based on brush size

III. Layer Management Basics

// Visibility Control
public setCanView(canView?: boolean) {
  this.canView = (canView !== undefined) ? canView : !this.canView;
}

// Layer Locking
public setLock(lock?: boolean) {
  this.lock = (lock !== undefined) ? lock : !this.lock;
}
Enter fullscreen mode Exit fullscreen mode
  • State Toggle: Supports both direct setting and toggle modes
  • Property Encapsulation: Maintains controlled state access through getters/setters

Top comments (0)

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay