2022.08.07

Node.jsで画像生成(@napi-rs/canvasを使う場合)【その8: 円形の中に画像を配置】

この記事でのゴール

Canvasに画像ファイルを読み込んで円形の形で切り取って表示する。

今回はローカルのファイルを利用します。

前提条件

以下をインストール済みであること

  • Node.js(16系)
  • npm または yarn

使うもの

前回までと同じ

ファイル構成

── output.png
├── package.json
├── src
│   ├── fonts
│   │   └── NotoSansJP-Bold.otf
│   ├── images
│   │   └── img-1.jpg // 読み込み用の画像ファイル
│   ├── index.ts
│   └── libs
│       └── loadImage.ts

読み込みに使用する画像

スクリプト

index.ts

import { promises } from 'fs'
import { join } from 'path'
import { Image, createCanvas, GlobalFonts } from '@napi-rs/canvas'
import { loadImage } from './libs/loadImage'

interface Positions {
  x: number
  y: number
}

interface Sizes {
  width: number,
  height: number
}

interface FitSizePositions {
  width: number,
  height: number,
  x: number,
  y: number
}

const OUTPUT_FILE_NAME: string = 'output' // 出力する画像の名前
const OUTPUT_FILE_EXTENSION: string = 'png' // 出力する画像の拡張子
const OUTPUT_FILE: string = `${OUTPUT_FILE_NAME}.${OUTPUT_FILE_EXTENSION}` // 拡張子を含む画像ファイル名

const CANVAS_SIZE: Sizes = { width: 800, height: 800 }
const CANVAS_BACKGROUND_COLOR: string = 'rgba(20, 30, 200, 0.5)' // 画像の背景色

const IMAGE_URL: string = join(__dirname, 'images', 'img-1.jpg') // 画像ファイルのパス

// 円形画像のサイズと座標
const CLIP_IMAGE_CIRCLE_RADIUS: number = 100
const CLIP_IMAGE_CIRCLE_POSITION: Positions = { x: 400, y: 400 }

const canvas = createCanvas(CANVAS_SIZE.width, CANVAS_SIZE.height)
const ctx = canvas.getContext('2d')

ctx.fillStyle = CANVAS_BACKGROUND_COLOR
ctx.fillRect(0, 0, CANVAS_SIZE.width, CANVAS_SIZE.height)

/**
 * 画像が縦長であることを判定する
 * @param imageWidth 元の画像の横の長さ
 * @param imageHeight 元の画像の縦の長さ
 * @returns { boolean }
 */
const isVerticalImage = (imageWidth: number, imageHeight: number): boolean => {
  return imageWidth < imageHeight
}

/**
 * 指定したサイズに対してぴったり収まる画像サイズを取得
 * @param imageWidth 元の画像の横の長さ
 * @param imageHeight 元の画像の縦の長さ
 * @param isVertical 縦長画像であるかどうか
 * @param baseWidth 基点とする横の長さ
 * @param baseHeight 基点とする縦の長さ
 * @returns { Sizes }
 */
const getFitSizes = (imageWidth: number, imageHeight: number, isVertical: boolean, baseWidth: number, baseHeight: number): Sizes => {
  const width = isVertical ? baseWidth : imageWidth / imageHeight * baseWidth
  const height = isVertical ? imageHeight / imageWidth * width : baseHeight
  return { width, height }
}

/**
 * 指定したサイズに対してぴったり収まるように配置するための座標を取得
 * @param imageWidth getFitSizesで得た配置画像の横の長さ
 * @param imageHeight getFitSizesで得た配置画像の縦の長さ
 * @param isVertical 縦長の画像であるかどうか
 * @param baseWidth 基点とする横の長さ
 * @param baseHeight 基点とする縦の長さ
 * @returns { Positions }
 */
const getFitPositions = (imageWidth: number, imageHeight: number, isVertical: boolean, baseWidth: number, baseHeight: number): Positions => {
  const xPosition = isVertical ? 0 : baseWidth / 2 - (imageWidth / 2)
  const yPosition = isVertical ? (baseHeight / 2 - imageHeight / 2): 0
  return { x: xPosition, y: yPosition }
}

const getFitSizePosition = (imageWidth: number, imageHeight: number, baseWidth: number, baseHeight: number): FitSizePositions => {
  const isVertical = isVerticalImage(imageWidth, imageHeight)
  const sizes = getFitSizes(imageWidth, imageHeight, isVertical, baseWidth, baseWidth)
  const positions = getFitPositions(sizes.width, sizes.height, isVertical, baseWidth, baseHeight)

  return { width: sizes.width, height: sizes.height, x: positions.x, y: positions.y }
}

const createClipPath = async () => {
  ctx.beginPath()
  ctx.arc(CLIP_IMAGE_CIRCLE_POSITION.x, CLIP_IMAGE_CIRCLE_POSITION.y, CLIP_IMAGE_CIRCLE_RADIUS, 0, 2 * Math.PI)
  ctx.clip()
}


// 画像を配置
const setImageFile = async () => {

  await createClipPath()

  const imageFile = await loadImage(IMAGE_URL)
  const canvasImage = new Image()
  canvasImage.src = imageFile

  const imageFitPosition = getFitSizePosition(canvasImage.naturalWidth, canvasImage.naturalHeight, CLIP_IMAGE_CIRCLE_RADIUS * 2, CLIP_IMAGE_CIRCLE_RADIUS * 2)

  canvasImage.width = imageFitPosition.width
  canvasImage.height = imageFitPosition.height
  ctx.fillStyle = 'rgba(0, 0, 0, 1)'
  ctx.drawImage(
    canvasImage,
    imageFitPosition.x + CLIP_IMAGE_CIRCLE_POSITION.x - CLIP_IMAGE_CIRCLE_RADIUS,
    imageFitPosition.y + CLIP_IMAGE_CIRCLE_POSITION.y - CLIP_IMAGE_CIRCLE_RADIUS,
    imageFitPosition.width,
    imageFitPosition.height
  )
}

const init = async(): Promise<void> => {
  await setImageFile()
  const imgData = await canvas.encode('png')
  await promises.writeFile(join(__dirname, `../${OUTPUT_FILE}`), imgData)
}

init()

やっていることは前回とほぼ同じで、四角形が円形に変わっただけです。

  1. 元の画像のサイズを取得
  2. 1から元の画像が縦長であるかを判定
  3. Canvasに配置する円形に対してぴったり配置するためのサイズを計算
  4. 3を利用して、円形に対してぴったり配置するための座標を計算する
  5. 3と4を利用してサイズと配置座標を指定した画像を配置

結果