描写関係

最小キャンバスの作成例

SwiftUI 側(中間処理)

updateNSView()が呼ばれるタイミング

1. 監視対象のプロパティが変更された時

あなたのコードでは以下が監視されています:

2. 親ビューが再描画された時

3. ビューが再表示された時

  • ウィンドウがアクティブになった時
  • タブを切り替えた時
  • など

簡単なまとめ

updateNSView()は以下の時に呼ばれます:

  1. 受け取ったの任意のプロパティが変更された時
  2. cg_imageが変更された時
  3. 親ビューが再描画された時

重要ポイント:

  • 思ったより頻繁に呼ばれる可能性がある
  • 重い処理は条件付きで実行する(boolなどで判定して処理を走らせない(returnさせるとか))
  • デバッグ用にprint()を入れて確認するのがおすすめ

NSView 本体

SwiftUI から使う

layout()が呼ばれるタイミング

基本概念

layout()ビューのサイズや位置が変わった時に自動的に呼ばれるメソッドです。

override func layout() {
    super.layout()
    print("📐 layout() が呼ばれました")
    // サブビューやレイヤーの配置を更新
}

呼ばれるタイミング一覧

1. ビューのサイズが変更された時

// ウィンドウをリサイズ
ユーザーがウィンドウの端をドラッグ
→ layout() が呼ばれる

// プログラムでサイズ変更
canvasView.frame.size = CGSize(width: 800, height: 600)
→ layout() が呼ばれる

2. ビューが初めて表示される時

// ビューが画面に追加された直後
let canvasView = CanvasNSView()
parentView.addSubview(canvasView)
→ layout() が呼ばれる

3. needsLayoutが設定された時

// 明示的にレイアウト更新を要求
canvasView.needsLayout = true
// 次の描画サイクルで layout() が呼ばれる

4. スクロールした時(場合による)

// NSScrollViewの中でスクロール
ユーザーがスクロール
→ 状況によっては layout() が呼ばれる

5. ウィンドウの状態が変わった時

// ウィンドウが最小化から復帰
// フルスクリーンのオン/オフ
// 別のディスプレイに移動
→ layout() が呼ばれる

パターン1: 初回表示

[アプリ起動]
↓
CanvasView.makeNSView() 呼び出し
↓
CanvasNSView を作成
↓
setImage() 呼び出し
↓
NSScrollView に追加
↓
📐 layout() が呼ばれる ← 初回

パターン2: ウィンドウリサイズ

[ユーザーがウィンドウを拡大]
↓
NSScrollView のサイズが変わる
↓
CanvasNSView のサイズも変わる
↓
📐 layout() が呼ばれる

パターン3: 画像変更

[新しい画像を読み込む]
↓
updateNSView() 呼び出し
↓
setImage() 呼び出し
↓
self.frame = ... でサイズ変更
↓
📐 layout() が呼ばれる

実験: 呼び出し回数を確認

override func layout() {
    super.layout()
    print("📐 layout() 呼び出し - frame: \(frame)")
    
    root_layer.frame = bounds
    // ... 既存のコード
}

出力例:

📐 layout() 呼び出し - frame: (0.0, 0.0, 800.0, 600.0)
[ウィンドウリサイズ]
📐 layout() 呼び出し - frame: (0.0, 0.0, 1000.0, 700.0)
[画像変更]
📐 layout() 呼び出し - frame: (0.0, 0.0, 1200.0, 900.0)

注意: 何度も呼ばれる可能性

layout()頻繁に呼ばれる可能性があります:

// ❌ 重い処理は避ける
override func layout() {
    super.layout()
    
    // これは毎回実行される
    for i in 0..<1000000 {
        // 重い計算...
    }
    // → パフォーマンス低下!
}

// ✅ 軽い処理のみ
override func layout() {
    super.layout()
    
    // フレームの更新程度ならOK
    root_layer.frame = bounds
    shape_layer_root.frame = root_layer.bounds
}

layout() vs setImage()

メソッド呼び出しタイミング用途
setImage()画像を設定する時(1回)初期配置・画像変更時
layout()サイズ変更の度(複数回)サイズ変更への対応

使い分けの例

func setImage(_ cg_image: CGImage) {
    // 🔥 初期配置はここで
    let imageSize = CGSize(width: cg_image.width, height: cg_image.height)
    let canvasSize = CGSize(width: 1200, height: 900)
    
    self.frame = CGRect(origin: .zero, size: canvasSize)
    
    let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)
    
    image_layer_root.bounds = CGRect(origin: .zero, size: imageSize)
    image_layer_root.position = center
    // ... 他のレイヤーも配置
}

override func layout() {
    super.layout()
    
    // 🔥 サイズ変更時の調整はここで
    root_layer.frame = bounds
    
    // レイヤーの位置を再計算(必要な場合)
    // ※ 中央配置を維持したい場合は再計算が必要
}

よくあるパターン

パターンA: 一度だけ配置(シンプル)

// setImage()で完全に配置
func setImage(_ cg_image: CGImage) {
    // すべての配置処理
}

// layout()は最小限
override func layout() {
    super.layout()
    root_layer.frame = bounds
}

パターンB: 動的に再配置(柔軟)

// setImage()は画像のみ
func setImage(_ cg_image: CGImage) {
    image_layer_root.contents = cg_image
    needsLayout = true  // layout()を呼び出すよう要求
}

// layout()で毎回配置を計算
override func layout() {
    super.layout()
    
    // 現在のサイズに応じて中央配置を再計算
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
    image_layer_root.position = center
    // ... 他のレイヤーも
}

デバッグ用コード

private var layoutCallCount = 0

override func layout() {
    layoutCallCount += 1
    print("📐 layout() 呼び出し回数: \(layoutCallCount)")
    print("   frame: \(frame)")
    print("   bounds: \(bounds)")
    
    super.layout()
    
    root_layer.frame = bounds
}

あなたのコードでのおすすめ

func setImage(_ cg_image: CGImage) {
    // 画像サイズ
    let imageSize = CGSize(width: cg_image.width, height: cg_image.height)
    
    // キャンバスサイズ(大きめに)
    let canvasSize = CGSize(width: 1200, height: 900)
    self.frame = CGRect(origin: .zero, size: canvasSize)
    
    // 中央位置
    let center = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)
    
    // すべてのレイヤーを配置
    [image_layer_root, layered_images_root, drawing_layer_root,
     shape_layer_root, text_layer_root].forEach { layer in
        layer.bounds = CGRect(origin: .zero, size: imageSize)
        layer.position = center
    }
    
    // 画像を設定
    image_layer_root.contents = cg_image
    // ...
}

override func layout() {
    super.layout()
    
    // root_layerだけ更新(他のレイヤーは setImage() で配置済み)
    root_layer.frame = bounds
}

まとめ

タイミング説明頻度
初回表示ビューが画面に追加された時1回
サイズ変更ウィンドウリサイズなど複数回
画像変更setImage()でフレームが変わった時画像変更時
needsLayout明示的に要求した時必要に応じて
ウィンドウ状態変化最小化復帰、フルスクリーンなど時々

重要ポイント:

  • layout()自動的に呼ばれる
  • 頻繁に呼ばれる可能性がある
  • 重い処理は避ける
  • レイアウトの調整が主な用途

View を @State にするのが良くない理由

作り直す、リセットするときは id()モディファイアを使う

NSScreen.main?.backingScaleFactor ?? 2.0

NSScreen.main?.backingScaleFactor ?? 2.0

 は、現在のメインディスプレイのバッキングスケールファクター(Backing Scale Factor)を取得するためのコードです。この値は、macOSアプリでの高解像度(Retina)ディスプレイのサポートにおいて重要です。 

効果と役割

このコードの主な効果と役割は以下の通りです。

  • 解像度の違いを吸収する: macOS (AppKit) では、画面の座標系はポイント (points) という論理的な単位で扱われます。一方、実際の描画が行われるバッキングストア(バックエンドのピクセルバッファ)は物理的なピクセル (pixels) で構成されます。backingScaleFactor は、1ポイントあたりの物理ピクセル数を表します。
  • Retina対応の判定:
  • 手動描画の調整: 通常、ほとんどの標準的なUI要素や自動レイアウトでは、システムが自動的にスケーリングを処理するため、開発者がこの値を直接使用する必要はほとんどありません。しかし、OpenGLやカスタムの低レベルグラフィックスAPIを使用して手動で描画を行う場合や、キャッシュするビットマップ画像のサイズを決定する場合など、明示的なピクセル単位の制御が必要な稀なケースで、適切な解像度で描画するためにこの値を使用します。
  • デフォルト値 ?? 2.0:

まとめ

このコードは、現在の画面がRetinaディスプレイかどうかを判断し、適切なグラフィックスの描画スケールファクターを決定するためのものです。特にカスタム描画コードにおいて、鮮明な表示を実現するために使用されます。 

CGImage を CALayer に変換

なぜこの設定が必要か(重要)

① contents = cgImage

  • CALayer は CGImage を直接持てる
  • NSImage 変換不要(高速)

② contentsGravity = .resizeAspect

  • アスペクト比保持
  • Windowsペイント的挙動

③ contentsScale

  • Retina 対応(必須)
  • これが無いと拡大時にボケる

④ bounds

  • CALayer は frame ではなく bounds が重要
  • 後で transform / zoom をかけるため

使い方例(Canvasに配置)

座標系の注意(macOS特有)

macOS の CALayer は:

  • 原点:左下
  • SwiftUI / NSView:左上

よくある対策

👉 これを root に設定すると
マウス座標と描画が一致

拡大・縮小の準備

文字・線を載せる準備 未確認

👉 同じ bounds に揃えるのがコツ

「編集結果」を CGImage に戻す場合(参考)未確認

空のビットマップを作る

Color と NSColor のキャスト

NSColor から SwiftUI Color への変換

SwiftUI Color から NSColor への変換

penで使っていた描写コード

塗りつぶし関数

リッチバージョン(体感わからんかった、状況揃える必要ありそう)

動作説明

  1. クリックした位置の色を取得
  2. 同じ色の連続した領域を検索(4方向:上下左右)
  3. 選択した色で塗りつぶす
  4. Undo対応

特徴

  • ✅ スタックベースのアルゴリズム(再帰なしで安全)
  • ✅ Undo/Redo対応
  • ✅ 高解像度対応(Retinaディスプレイ対応)
  • ✅ 透明度も考慮

パフォーマンス向上(オプション)

大きな画像で遅い場合は、許容誤差を追加できます:

CGImage → CALayer

CALayer → CGImage

CALayer.contents

CALayer.contents の効果

基本的な効果

image_layer.contents = cg_image

効果: レイヤーが画像を表示するようになる

動作の流れ

1. 画像がない状態(初期状態)

let layer = CALayer()
layer.backgroundColor = NSColor.blue.cgColor
// → 青い四角形が表示される(画像なし)
┌─────────┐
│         │
│  青色   │  ← backgroundColorのみ
│         │
└─────────┘

2. 画像を設定した状態

layer.contents = cgImage
// → 画像が表示される
┌─────────┐
│  🖼️     │
│  画像   │  ← contentsで設定した画像
│         │
└─────────┘

contentsに設定できるもの

用途
CGImage画像表示(最も一般的)layer.contents = cgImage
nil画像をクリアlayer.contents = nil
NSImage ❌直接は不可CGImageに変換が必要

CGImageのみ使える理由

// ✅ これはOK
layer.contents = cgImage  // CGImage型

// ❌ これはダメ
layer.contents = nsImage  // NSImage型は直接使えない

// ✅ NSImageからCGImageに変換すればOK
if let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) {
    layer.contents = cgImage
}

あなたのコードでの役割

全体の流れ

func setImage(_ cg_image: CGImage) {
    // 1️⃣ レイヤーに画像を設定(これで画像が表示される!)
    image_layer.contents = cg_image
    
    // 2️⃣ 画像の表示方法を設定
    image_layer.contentsGravity = .resizeAspect  // アスペクト比を保持
    
    // 3️⃣ Retinaディスプレイ対応
    image_layer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
    
    // 4️⃣ レイヤーのサイズを画像サイズに合わせる
    let size = CGSize(width: cg_image.width, height: cg_image.height)
    image_layer.bounds = CGRect(origin: .zero, size: size)
    
    // 5️⃣ レイヤーの位置を中央に
    image_layer.position = CGPoint(x: size.width / 2, y: size.height / 2)
    
    // 6️⃣ 描画レイヤーも同じサイズにセットアップ
    setupdrawingLayer(size: size)
}

各プロパティの意味

contents – 何を表示するか

image_layer.contents = cg_image  // ← この画像を表示

contentsGravity – どう配置するか

image_layer.contentsGravity = .resizeAspect
効果
.resizeAspectアスペクト比を保持してフィット
.resizeAspectFillアスペクト比を保持して塗りつぶし
.resizeレイヤーサイズに合わせて伸縮
.center中央に配置(サイズそのまま)

contentsScale – 解像度

image_layer.contentsScale = 2.0  // Retina対応(2倍解像度)

効果: ぼやけない高解像度表示

// contentsScale = 1.0 の場合
┌─────┐
│ 🔲  │  ← ぼやける(低解像度)
└─────┘

// contentsScale = 2.0 の場合
┌─────┐
│ ⬛  │  ← くっきり(高解像度)
└─────┘

実験: contentsの有無による違い

パターンA: contentsなし

let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
layer.backgroundColor = NSColor.red.cgColor
// contents は nil(デフォルト)

表示: 赤い四角形のみ

パターンB: contentsあり

let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
layer.backgroundColor = NSColor.red.cgColor
layer.contents = someImage  // ← 画像を設定

表示: 画像が表示される(backgroundColorは背後に)

パターンC: contentsのみ

let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
layer.contents = someImage  // 画像のみ
// backgroundColor は nil

表示: 画像のみ(背景色なし、透明部分は透過)

よくある使い方

1. 画像レイヤー(あなたのコード)

let imageLayer = CALayer()
imageLayer.contents = cgImage  // ← 背景画像として使用

2. 画像を更新

// 古い画像を新しい画像に置き換え
imageLayer.contents = newCGImage
// → 即座に表示が更新される

3. 画像をクリア

imageLayer.contents = nil  // ← 画像を削除
// → backgroundColorのみ表示される

4. アニメーション

// contentsを変えることでアニメーションできる
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
imageLayer.contents = newImage
CATransaction.commit()
// → 1秒かけて画像が切り替わる

contents vs backgroundColor

プロパティ用途
contents画像を表示CGImage?
backgroundColor単色を表示CGColor?

両方設定した場合: backgroundColorが背景、contentsが前面に表示

layer.backgroundColor = NSColor.blue.cgColor  // 背景: 青
layer.contents = cgImage  // 前面: 画像
// → 画像の透明部分から青色が見える

パフォーマンスの注意点

✅ 効率的

// 一度設定したらそのまま
imageLayer.contents = cgImage

❌ 非効率

// 毎フレーム更新は重い
func updateEveryFrame() {
    imageLayer.contents = generateNewImage()  // 毎回生成は遅い
}

💡 最適化

// 変更があった時だけ更新
func updateImage(_ newImage: CGImage) {
    guard imageLayer.contents as? CGImage !== newImage else { return }
    imageLayer.contents = newImage
}

まとめ

image_layer.contents = cg_image の効果

  1. レイヤーが画像を表示するようになる
  2. レイヤーのビジュアルコンテンツを設定
  3. 即座に画面に反映される
  4. 何度でも変更可能(画像の切り替え)
  5. nilを設定すると画像がクリアされる(contents = nil )

あなたのコードでは

image_layer.contents = cg_image

これによりペイントアプリの背景画像image_layerに表示されます。その上に描画レイヤー、図形レイヤー、テキストレイヤーが重なる構造になっています。

┌─────────────────┐
│  text_layer     │  ← テキスト
├─────────────────┤
│  shape_layer    │  ← 図形
├─────────────────┤
│  drawing_layer  │  ← ペン描画
├─────────────────┤
│  image_layer    │  ← 背景画像(contents = cg_image)
└─────────────────┘

よく使う関連プロパティ

contents を設定する際、以下のプロパティを併用して表示を調整します。

  1. contentsGravity:
    画像のサイズがレイヤーと異なる場合に、どのように配置・拡大縮小するかを決定します(例: .resizeAspect.center) [3]。
  2. contentsScale:
    Retinaディスプレイなどの解像度に対応させるため、通常 UIScreen.main.scale を設定します。これを忘れると画像がぼやける原因になります [4]。
  3. contentsRect:
    画像の一部だけを切り取って表示したい場合に使用します(0.0〜1.0 の範囲で指定) [1]。
  4. magnificationFilter: コンテンツが拡大・縮小される際のフィルタリング方法(画質)を制御します(例: .linear.nearest)。

実装例 (Swift)

.addSublayer() との違い

CALayer.contents と .addSublayer() は、どちらも「レイヤーに何かを表示する」ためのものですが、「中身を塗る」か「子分を増やすか」という決定的な違いがあります。

ざっくり例えると、「1枚の画用紙(Layer)」に対して:

  • contents: 画用紙に直接写真をペタッと貼る(中身の書き換え)
  • .addSublayer(): 画用紙の上に、別の小さな紙を重ねて置く(階層の追加)

主な違いの比較

特徴contents (プロパティ)addSublayer() (メソッド)
本質レイヤーの「中身(画像)」を指定レイヤーの「子要素」を追加
階層構造増えない(そのレイヤー自身の属性)増える(親子関係が形成される)
主な用途背景画像やアイコンの表示複雑な図形、UIコンポーネントの構築
メモリ/負荷低い(単一の描画ユニット)contentsよりは高い(管理対象が増える)
タッチ判定そのレイヤー1つで完結子レイヤーごとに個別に判定可能

使い分けのイメージ

1. contents を使うとき

「このレイヤー自体に画像を表示させたい」という場合です。

  • ボタンの背景に画像を入れたい
  • アバターのアイコン画像を表示したい
  • メリット: 構造がシンプルで、メモリ消費も最小限です。

2. addSublayer() を使うとき

「複数の要素を組み合わせて一つのパーツを作りたい」という場合です。

  • 背景レイヤーの上に、影用のレイヤーや、枠線用のレイヤーを重ねたい
  • 動くキャラクターの「腕」や「足」を別々のパーツとして制御したい
  • メリット: 重なり順(Z順序)を自由に入れ替えたり、特定の子要素だけを個別にアニメーションさせたりできます。

併用することもあります

実務では、「親レイヤーに contents で背景画像を設定し、その上に addSublayer で装飾用のパーツを載せる」といった形で組み合わせて使うのが一般的です。

Apple公式: Core Animation Programming Guide では、レイヤーの階層構造(Layer Hierarchy)についての詳細が解説されています。

addSublayer()とは?

基本概念

addSublayer()レイヤーの親子関係を作るメソッドです。

parentLayer.addSublayer(childLayer)

レイヤー階層のイメージ

root_layer (親)
├── image_layer (子)
├── drawing_layer (子)
├── shape_layer_root (子)
│   ├── shape1 (孫)
│   ├── shape2 (孫)
│   └── shape3 (孫)
└── text_layer_root (子)
    ├── text1 (孫)
    └── text2 (孫)

重要な特徴

1. 表示されるための条件

レイヤーが画面に表示されるには:

  • ✅ 親レイヤーに追加されている(addSublayer()済み)
  • ✅ 親レイヤーが表示されている
  • ✅ 自分自身がisHidden = false
// ❌ 表示されない(どこにも追加されていない)
let layer = CAShapeLayer()
layer.path = somePath

// ✅ 表示される(親レイヤーに追加)
shape_layer_root.addSublayer(layer)

2. 重ね順(Z-order)

後から追加したレイヤーが上(手前)に表示されます。

root_layer.addSublayer(layerA)  // 一番下
root_layer.addSublayer(layerB)  // layerAの上
root_layer.addSublayer(layerC)  // 一番上(最前面)

表示順序:

layerC  ← 最前面(3番目に追加)
layerB  ← 中間(2番目に追加)
layerA  ← 最背面(1番目に追加)

3. 座標系

子レイヤーの座標は親レイヤーの座標系で解釈されます。

// 親レイヤーの(50, 50)の位置に配置
childLayer.frame = CGRect(x: 50, y: 50, width: 100, height: 100)
parentLayer.addSublayer(childLayer)

よく使うメソッド

レイヤーの追加・削除

// 追加
parentLayer.addSublayer(layer)

// 特定の位置に挿入
parentLayer.insertSublayer(layer, at: 0)  // 一番下に挿入
parentLayer.insertSublayer(layer, above: otherLayer)  // 指定レイヤーの上に
parentLayer.insertSublayer(layer, below: otherLayer)  // 指定レイヤーの下に

// 削除
layer.removeFromSuperlayer()  // 親から自分を削除

// 全ての子レイヤーを削除
parentLayer.sublayers?.forEach { $0.removeFromSuperlayer() }
// または
parentLayer.sublayers?.removeAll()  // 配列をクリア(自動的に削除される)

レイヤーの移動(重ね順変更)

// 一度削除して再追加 → 最前面に
layer.removeFromSuperlayer()
parentLayer.addSublayer(layer)

// または
func bringToFront(_ layer: CALayer) {
    layer.removeFromSuperlayer()
    parentLayer.addSublayer(layer)
}

あなたのコードでの使用例

1. 初期設定(setupLayers()

// ルートレイヤーに子レイヤーを追加 → 基本構造を作る
root_layer.addSublayer(image_layer)
root_layer.addSublayer(drawing_layer)
root_layer.addSublayer(shape_layer_root)
root_layer.addSublayer(text_layer_root)

意味: 画面に表示されるレイヤー構造を構築

2. 図形の追加(mouseDown()

// 新しく作った図形をshape_layer_rootの子として追加
shape_layer_root.addSublayer(layer)

意味:

  • 図形を管理用レイヤー(shape_layer_root)の子にする
  • これにより図形が画面に表示される
  • shape_layer_rootは複数の図形をまとめて管理

3. 図形の削除(Undo時)

layer.removeFromSuperlayer()  // 親(shape_layer_root)から削除

意味: レイヤーツリーから外す → 画面から消える

4. 図形を最前面に(移動モード)

func bringShapeLayerToFront(_ layer: CAShapeLayer) {
    layer.removeFromSuperlayer()  // 一旦削除
    shape_layer_root.addSublayer(layer)  // 再追加 → 最前面に
}

意味: 選択した図形を他の図形より手前に表示

よくある疑問

Q1: なぜshape_layer_rootに追加するの?

A: グループ管理のためです。

root_layer
└── shape_layer_root ← 図形専用の親レイヤー
    ├── shape1
    ├── shape2
    └── shape3

メリット:

  • すべての図形をまとめて操作できる
  • 図形だけを非表示にできる
  • 重ね順の管理が簡単
  • レイヤー順序の変更が容易

Q2: addSublayer()しないとどうなる?

A: 画面に表示されません。

// これだけでは表示されない
let layer = CAShapeLayer()
layer.path = somePath
layer.strokeColor = NSColor.red.cgColor
// → どこにも追加されていないので見えない

// 親レイヤーに追加して初めて表示される
shape_layer_root.addSublayer(layer)
// → 表示される!

Q3: レイヤーを移動するには?

Aframeを変更するだけでOK(親子関係はそのまま)

// 既に追加済みのレイヤーを移動
layer.frame = CGRect(x: 100, y: 100, width: 50, height: 50)
// → 親子関係は変わらず、位置だけ変わる

まとめ

操作メソッド結果
表示するparent.addSublayer(child)画面に表示される
消すchild.removeFromSuperlayer()画面から消える
最前面に削除 → 再追加他のレイヤーより手前に
位置変更child.frame = ...親子関係はそのまま移動

重要addSublayer()は「レイヤーツリーに参加させる」という意味で、これによって初めて画面に表示され、イベントを受け取れるようになります。

レイヤーを追加する

ルートレイヤーにaddする

1. CanvasNSViewsetupLayers()を修正

2. layout()メソッドに追加

レイヤー順を変更する

addSublayer

配列の要素を入れ替えて操作

zPosition

外部ViewのListで.onMoveを使ってレイヤー順を変更する

レイヤー順変更Viewを作成

メインViewに追加

CGContext.makeImage()

CGContext.makeImage()

 は、現在ビットマップコンテキストに描画されている内容を「スナップショット」として取り出し、CGImage オブジェクトを作成するためのメソッドです。

主な効果と特徴は以下の通りです:

1. 描画内容の静止画化

コンテキストに対して行った線、図形、テキスト、画像などの描画操作を、一つの独立した画像データ(CGImage)として生成します。

2. イミュータブル(不変)なコピーの作成

作成された CGImage は、その時点のコンテキストの状態を保持したコピーです。そのため、makeImage() を実行した後にコンテキストにさらに描画を加えても、出力済みの画像には影響しません。

3. メモリ効率の良い共有

Core Graphics は「Copy-on-write」の仕組みを利用するため、画像データが実際に変更されない限り、メモリを効率的に管理します。

4. 主な利用シーン

  • カスタム描画の画像保存draw(_:) メソッド内などで描いたものを UIImage に変換して保存やシェアをする。
  • 画像加工: 既存の画像にフィルタや合成処理を施した後、最終的な結果を取り出す。
  • オフスクリーンレンダリング: 画面に表示させない裏側のコンテキストで画像を作成する。

実装例 (Swift)

要約すると、CGContext.makeImage()は、コンテキスト内の描画結果を確定して画像データとして抽出するための重要な役割を果たします。 

注意点:
makeImage() は呼び出された時点でのビットマップをキャプチャするため、ループ内などで頻繁に呼び出すとパフォーマンスに影響を与える可能性があります。必要なタイミングで一度だけ呼び出すのが理想的です。

CGContext

CGContext は、いわば**「デジタル上のキャンバス」「描画エンジン」**のような役割を果たします。

1. 引数の設定について

CGContext を作成する際、コンピュータに「どのような形式で色や透明度をメモリに保存するか」を伝える必要があります。

space: CGColorSpaceCreateDeviceRGB()

  • 役割: **「色の基準(カラースペース)」**を決定します 。
  • 内容: 「デバイス依存のRGB」を使用するという指定です。画面上で自然な色合い(赤、緑、青の組み合わせ)で描画するために標準的に使われます 。

bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue

  • 役割: **「1ピクセルごとのデータ構造(透明度の扱い)」**を決定します 。
  • premultiplied(乗算済みアルファ): 透明度を含む画像を描画する際、計算を高速化するためにあらかじめ「色値 × 透明度」を計算して保存する形式です。macOSやiOSの描画系(Core Graphics)で最も推奨される効率的な設定です。
  • Last: データの並び順が「R, G, B, A(アルファ)」の順であることを示します。

2. ctx.draw の動作について

「この .draw は何度でも使用できるのですか?その際は上に被さるように描写される認識でいいですか?」

その通りです!認識は完全に合っています。

  • 何度でも使用可能: コード内でも、ループ(for layer in sortedLayers)の中で何度も ctx.draw や独自の描画関数(renderContainerLayers など)が呼ばれています 。
  • 重ね塗り(画家のアルゴリズム): 後から実行された描画コマンドは、先に描画された内容の上に**上書き(上乗せ)**されます。
  • 順序の重要性: そのため、添付コードでは TSO?.layer_order に基づいてレイヤーをソート(sortedLayers)し、正しい重なり順で描画されるように制御されています 。

補足:Core Graphics の座標系

コード内にコメントアウトされた部分がありますが 、Core Graphics(CGContext)は標準では左下が原点 (0,0)です。macOSの NSView の標準座標系と同じですが、画像データとして書き出す際に上下が反転して見える場合は、コンテキストの座標変換(scaleBy(x: 1.0, y: -1.0) など)が必要になることがあります。

この瞬間に決まるもの

  • ビットマップの 
  • ビットマップの 高さ
  • 描画可能な ピクセル領域

**「描画状態マシン」**が1つ生成される

以下の状態を内部に保持する:

  • strokeColor
  • fillColor
  • lineWidth
  • blendMode
  • transform
  • clip
  • path

だからこう書ける

描画が起きるのはここだけ

Layer 側との関係

drawingLayer.bounds と同サイズの image
Layer は image をそのまま表示
image 自体がはみ出していない

.textMatrix = .identity

CGContext.textMatrix = .identity

 は、テキスト描画に使用されるテキスト行列をデフォルトの状態(恒等変換)にリセットする効果があります。これにより、テキストは回転、拡大縮小、傾斜などの変換が適用されていない、標準の向きとサイズで描画されます。 

効果と重要性

  • デフォルトのテキスト描画: identity 行列(変換なし)を設定することで、Core Graphicsのデフォルトの座標系に従ってテキストが描画されます。
  • 描画の信頼性確保: NSAttributedString.draw(in:) のような他の描画関数は、内部的にテキスト行列を変更することがあります。そのため、Core Graphics(またはCore Text)で直接テキストを描画する前に、この行を実行してテキスト行列を既知のデフォルト状態に戻すことが推奨されます。
  • 予測可能な動作: テキスト描画の向きや位置が意図せず変更されるのを防ぎ、予測可能な描画結果を保証します。
  • グラフィックスステートとは独立: テキスト行列はグラフィックスコンテキスト(CGContext)の属性ですが、saveGState() や restoreGState() のようなグラフィックスステートの保存・復元操作では保存されないことに注意が必要です。そのため、必要に応じて明示的に設定する必要があります。 

まとめ

CGContext.textMatrix = .identity は、その時点で行列にどのような変更が加えられていたとしても、テキストが標準的でまっすぐな状態で描画されるようにするための初期設定またはリセット操作として機能します。 

.saveGState()

CGContext.saveGState()

は、現在のグラフィックス状態のコピーをコンテキストのスタックに保存するために使用されます。これにより、以降の描画操作で変換(回転、拡大縮小、移動)、色、線幅などの属性を変更しても、後で元の状態に簡単に戻すことができます。 

saveGState() の効果と使い方

  • 状態の保存saveGState() が呼び出されると、その時点での描画設定(グラフィックス状態)がメモリ内のスタックにプッシュ(保存)されます。
  • 一時的な変更: 保存後、コンテキストの設定(例: context.translateBy(x: y:) や context.setFillColor(...) など)を自由に変更して描画を行うことができます。
  • 状態の復元: 変更後の描画が完了したら、context.restoreGState() を呼び出すことで、スタックの最上部にある保存された状態がポップ(復元)され、現在のグラフィックス状態が保存直前の状態に戻ります。 

使用例

例えば、「一部の図形だけ45度回転させて赤色で塗りたい」という場合:

・saveGState() で今の状態を保存。
・座標を回転させ、色を赤に変更。
・描画する。
restoreGState() を呼ぶ。
これで、「回転」や「赤色」の設定をいちいち手動で元に戻す(0度に戻す、元の色を再設定する)手間が省けます。

注意点

  • 描画内容は保存されないsaveGState() が保存するのはあくまで「描画パラメータ」(色、変換行列、クリッピングパスなど)であり、それまでに描画された画像やパス自体は保存されません
  • ペアで使うsaveGState() と restoreGState() は常にセットで使用することが意図されています。ネストして使用することも可能です。

.restoreGState()

CGContext.restoreGState()

 は、直前に保存したグラフィックスコンテキストの状態(グラフィックスステート)を復元するためのメソッドです。これ単体ではなく、必ず CGContext.saveGState() とセットで使われます。 

効果と目的

  • 描画設定の「元に戻す」を実現する: saveGState() を呼び出した時点の線の太さ、色、変換行列(回転・拡大縮小・移動)、クリッピング領域、ブレンドモードなどの描画パラメータをスタックに保存します。
  • 影響範囲を局所化する: 特定の描画処理(例: あるオブジェクトを描画するための回転や色の変更)の前後で saveGState() と restoreGState() を使用することで、その処理による描画設定の変更が、それ以降の他の描画に影響を与えないようにすることができます。
  • 複雑な描画を簡素化する: 変換行列など、元に戻すのが難しい、あるいは不可能な設定変更を簡単に行うことができます。 

使用例

複数の異なるスタイルで描画したい場合などに役立ちます。

このように、saveGState() と restoreGState() のペアは、描画コンテキストの状態管理を容易にし、描画コードを整理するために不可欠な仕組みです。

.concatenate()

CGContext.concatenate(_:)

 は、現在の描画コンテキストの CTM(現在の変形行列)に、指定した CGAffineTransform を掛け合わせる(合成する)メソッドです。

ひとことで言うと

「現在の状態を維持したまま、新しい変形(回転・拡大・移動など)を上乗せする」機能です。

効果

非可逆(描画状態の保存が必要): 一度 concatenate すると、元の座標系に戻すには逆行列をかけるか、context.saveGState() / context.restoreGState() を使って状態を復元する必要があります。

座標系の変更: 以降の描画すべてに、合成された変形が適用されます。

累積的: すでに scale や rotate がかかっている場合、その上からさらに変形が加わります。

注意点:計算の順序

CGAffineTransform の合成(concatenating)やコンテキストへの適用順序によって、結果が大きく変わります。 

移動してから回転: 指定した場所へ移動した後、その地点で回転します。

回転してから移動: 回転した「軸」に沿って移動します。

クリッピング領域とは

「クリッピング領域」とは、一言でいうと「描画がはみ出さないように設定する、型抜き(マスク)の境界線」のことです。

「曲線の先端」のことではなく、「ここから外側には描画しないでね」という目に見えない壁を作るイメージです。

1. クリッピング領域のイメージ

クッキー作りを想像してみてください。

  • 生地: キャンバス(描画対象)
  • クッキーの型: クリッピングパス(クリッピング領域)
  • 上からまぶす砂糖: 描画(色塗りや画像)

型を置いた状態で上から砂糖をまぶすと、型の内側だけに砂糖が残り、外側は綺麗なままですよね。これがクリッピングの効果です。

2. 具体的な使い方(Swift例)

例えば、四角い画像を「円形」に切り抜いて表示したい場合に便利です。

3. 「曲線の先端」については?

「曲線の先端(端っこ)」のデザインを制御する設定は、別にあります。

  • Line Cap (ラインキャップ): 線の端を「ぶつ切り」にするか「丸く」するか。
  • Line Join (ラインジョイン): 線と線の「つなぎ目」を角にするか丸くするか。

まとめ

  • クリッピング領域: 「型抜き」のこと。特定の形の中だけに絵を描きたい時に使う。
  • 曲線の先端: 「Line Cap」のこと。線の端っこの形状のこと。

saveGState() を使わずに clip() をしてしまうと、その後の描画がずっとその「型」の中に制限されてしまいます。なので、「この図形だけ型抜きしたい!」という時に、さっきの保存・復元がセットで活躍します。

図形をレイヤーにする例

崩れないレイヤーを作る

回転後にドラッグすると frame が壊れる
原因は
「回転済みレイヤーの frame / bounds / position を移動基準にしている」

対策として、一枚レイヤーを被せて移動はそれで行う、変形は中身のレイヤーだけで行う。

なぜ 45° が一番ひどいのか

frame は:

「回転後の bounds を含む最小の axis-aligned 矩形」

です。

45° の正方形を想像すると:

  • 回転するたびに
  • 外接矩形のサイズが変わる

つまり:

回転 → frame 変化
frame.midX を基準に position 再設定
→ 中心がズレる
→ 次のドラッグでさらにズレる

これを繰り返している。

他にもレイヤーをコンテナ化にして中に変形レイヤーを持つメリット

「transform で移動すると、回転後に逆方向に動く」これの対策になる

コンテナクラスの実装例

これをインスタンス化して中身の【contentLayer】に変形を担当させる。

なぜこれで全て解決するか

問題解決
回転後 frame が壊れるframe を使わない
45° でズレるposition は未回転
移動が逆になるposition は常に画面座標
Undocontainer + content で安定
FloodFill 境界content.transform を使えばOK

なぜ Adobe 系もこの方式か

  • Illustrator
  • Photoshop
  • PowerPoint
  • Figma

全部この構造

「動かす箱」と「変形する中身」を分ける

CGMutablePath()

CGMutablePath  は、Swift(Core Graphics)で図形の通り道(パス)を作成・編集するための「設計図」のようなものです。

主な効果と使い道は以下の通りです。

1. 複雑な図形を自由に作れる

直線、曲線(ベジェ曲線)、円弧、長方形などを組み合わせて、標準のパーツにはない独自の形を描画できます。

  • 効果: move(to:) でペンを置き、addLine(to:) や addCurve(to:) でなぞるように形を作れます。

2. メモリ効率の良い描画

一度作成したパスは、SKShapeNode(SpriteKit)や CAShapeLayer(UIKit/QuartzCore)に渡して再利用できます。

  • 効果: 同じ図形を何度も計算し直す必要がなく、描画パフォーマンスが向上します。

3. 当たり判定(ヒットテスト)に使える

描画だけでなく、「特定の点(タップした場所)が図形の中にあるか」を判定するのに非常に便利です。

  • 効果: path.contains(point) を使うことで、不規則な形のボタンやキャラクターへのタッチ判定が正確に行えます。

4. 途中で「変更」が可能

CGPath(不変)とは異なり、CGMutablePath は作成した後から内容を追加・変更できます。

  • 効果: アニメーションの途中で形を少しずつ変えたい場合などに、動的にパスを構築できます。

簡単な実装例

レイヤーにキーを付与する(Key-Value Coding (KVC) )

Swiftの CALayer は KVC に対応しているため、動的に新しいキーと値を追加できます。サブクラスを作るほどではない場合に便利です。

レイヤーメソッド、プロパティ

.removeFromSuperlayer()

表示を一旦クリアにする
オブジェクトは消えない

.name

CALayer には標準で name: String? というプロパティがあります。これに識別用の文字列を入れる方法です。

レイヤーを手前に出す

CGAffineTransform

Swiftの

CGAffineTransformは、2Dグラフィックスにおける「変形(アフィン変換)」を扱うための構造体です。

UIViewのtransformプロパティなどに設定することで、ビューの回転・拡大縮小・移動を簡単に行うことができます。 

主な役割

元の画像やビューの形を保ったまま、以下の3つの操作(およびその組み合わせ)を実行します。

  1. Translation(移動): 上下左右に動かす。
  2. Scale(拡大・縮小): 大きさを変える。
  3. Rotation(回転): 角度を変える。

基本的な使い方

1. 移動 (Translation)

現在の位置から指定した距離だけ移動させます。

2. 拡大・縮小 (Scale)

倍率を指定してサイズを変更します。

3. 回転 (Rotation)

指定した角度(ラジアン)で回転させます。

4. リセット (Identity)

変形を解除して元の状態に戻すには、.identity を使用します。

複数の変形を組み合わせる (Concatenating)

移動させてから回転させるなど、複数の変形を連結できます。

補足

CGAffineTransform は「端をドラッグして広げる」タイプのリサイズにはあまり向きません。

その理由は、CGAffineTransform が行うのはあくまで**「図形全体の拡大縮小(スケーリング)」**だからです。

リサイズ関数

CGRect(origin: .zero, size: rect.size) origin:について

CGRect(origin: .zero, size: rect.size)

 における origin: は、その CGRect の左上隅の座標を定義するものです。

origin: に .zero を指定することは、以下の座標を設定することと同じ意味になります。

  • x 座標: 0
  • y 座標: 0

これは、新しく作成される矩形が、親となるビューや座標系の左上隅(原点)から始まることを意味します。

使用例

このコードは、多くの場合、描画コンテキスト全体をカバーする矩形や、ビューの左上から始まる矩形を作成する際に使用されます。

例えば、draw(_:) メソッド内で現在のビュー全体を描画したい場合、引数として渡される rect(描画可能な領域のサイズを持つ)を使用して、原点から始まる新しい CGRect を作成します。

図形に値を記憶させる

レイヤーを拾う例

矩形、円の描写例

path について

CAShapeLayer における path は、そのレイヤーが描画する「図形の設計図」のようなものです。

CALayer(通常のレイヤー)は単なる四角い枠ですが、CAShapeLayer はその枠の中に 「ベジェ曲線(CGPath)」 という数学的な線を描くことができます。

1. Frame(枠)と Path(線)の違い

ここを理解するのが一番の近道です。

  • frame (CGRect): レイヤーが占有する「四角い領域」です。マウスイベントが反応する範囲や、背景色を塗る範囲を決めます。
  • path (CGPath): その領域の中に描かれる「具体的な形」です。

例えば、100×100の正方形の frame を持つレイヤーの中に、円形の path を描いた場合を考えてみましょう。

  • 角の部分は frame には含まれますが、path の中には含まれません。

2. なぜ Path を使うのか?

CAShapeLayer は path プロパティにパスを渡すだけで、自動的に滑らかな図形を描画してくれます。

3. 「当たり判定」での Path の強力さ

path を使うと「正確な図形の形」でマウス判定ができます。

Frameでの判定(大まか)

layer.frame.contains(point) を使うと、円の図形であっても、角の透明な部分をクリックした時に反応してしまいます。

Pathでの判定(正確)

layer.path?.contains(point) を使うと、**「円の内側をクリックしたときだけ」**反応させることができます。

4. パスの種類 (CGPath)

よく使うパスの作り方は以下の通りです。

図形作り方 (Swift)
矩形CGPath(rect: rect, transform: nil)
角丸矩形CGPath(roundedRect: rect, cornerWidth: 10, cornerHeight: 10, transform: nil)
円・楕円CGPath(ellipseIn: rect, transform: nil)
自由な線CGMutablePath() を作って move(to:) や addLine(to:) を使う

まとめ:設計図としての Path

CAShapeLayer にとって、frame は「キャンバスの場所と大きさ」であり、path は「そこに何を描くかの指示書」です。

現在のコードでは stroke.tool に応じて path.addRect や path.addEllipse を使い分けて設計図を作っています。これがまさに path の基本操作です!

レイヤーの座標プロパティ

一言でいうと、frame/bounds/position は「箱(容器)」の設定、path は「中身の形(絵)」の設定です。

1. 各プロパティの役割

プロパティ役割座標系
frame**「親レイヤーから見た」**自分の位置とサイズ親の座標系
bounds**「自分自身から見た」**描画範囲のサイズ自分の座標系
position**「親レイヤー内での」**自分の基準点の位置親の座標系
path**「自分の中に描く」**図形の具体的な設計図自分の座標系

2. 具体的なイメージ

frame (CGRect)

  • レイヤーを配置する「外枠」です。
  • frame.origin は親要素の左下(macOSの場合)からの距離を示します。
  • 注意: レイヤーを回転させたり拡大縮小(transform)したりすると、この値は正しく取得できなくなります。

bounds (CGRect)

  • レイヤー内部の「キャンバスの大きさ」です。
  • 通常、origin は (0, 0) です。
  • もし bounds.size を frame.size より小さくすると、はみ出た path は(masksToBounds が true の場合)切り取られます。

position (CGPoint)

  • レイヤーの「基準点(AnchorPoint)」が、親のどこにあるかを示します。
  • デフォルトではレイヤーの中心(anchorPoint = (0.5, 0.5))を指します。
  • frame.origin を動かすのと、position を動かすのは、結果として同じ「移動」になりますが、回転や拡大の中心点として使われるのがこの position です。

path (CGPath)

  • これが CAShapeLayer の本体です。
  • 重要なルール: path の座標は bounds の中での位置 を指定します。
  • 例:frame が (100, 100, 50, 50) のレイヤーに、(0, 0, 50, 50) の path を描くと、画面の (100, 100) の位置に図形が表示されます。

もし frame を設定せずに path だけを設定するとどうなるか?

  • レイヤー自体のサイズが (0, 0) になってしまいます。
  • 図形は見えますが、layer.frame.contains(point) などの当たり判定が一切効かなくなります(面積が0なので)。
  • そのため、「frame で当たり判定の箱を作り、path でその中に図形を描く」 という今の実装方法が macOS アプリ開発ではベストです。

まとめ:ドラッグ移動との関係

ドラッグ移動の実装で CGAffineTransform を使う場合、実は内部的には position が変化しています。

豆知識: > CGAffineTransform で移動させても path 自体のデータ(座標値)は書き換わりません。「描かれた絵」はそのままで、「額縁(レイヤー)」ごと移動しているイメージです。

移動(position)

point はドラッグ中のマウスポインターの位置、イベントで受け取ってる値
start_point はクリックした地点のマウスポインターの位置、イベント受け取り

リサイズ(bounds)

start_container_bounds はクリック地点の bounds
親レイヤーに連動して子レイヤー(実際に変形させたいレイヤー)をリサイズ

回転(setAffineTransform)

rotation_center はクリックした時のレイヤー中心地(position)
rotation_start_angle は↑から補助関数 angleBetween() で出した値

角度計算の補助関数、センターはレイヤーの中心(.position)
中心からクリック地点の角度を求める標準の三角関数 atan2 を使ってる

anchorPoint と position の違い

1. anchorPoint と position の役割の違い

一言でいうと、「どこを(Anchor)」 「どこに(Position)」 配置するかという関係です。

  • position (CGPoint):
  • anchorPoint (CGPoint):

結果として: レイヤーは、自身の anchorPoint が親の position と重なるように配置されます。

2. コードの意図:なぜ「補正(Offset)」が必要なのか?

添付された renderContainerLayers 内の処理は、CALayer が自動で行っている計算を CGContext(手動描画)上で再現しています。

なぜこれが必要か:

  1. 最初の containerTransform(positionへの移動)だけだと、キャンバスの原点 (0,0) が「レイヤーを配置したい地点」になります。
  2. このまま描画すると、レイヤーの「左下」がその地点にきてしまいます。
  3. そこで、anchorOffset(幅と高さの半分、つまり -0.5 倍)だけさらにキャンバスをずらすことで、**「レイヤーの中央が position の真上に来る」**ように調整しているのです。

この処理があるおかげで、後続の回転や拡大などの変形(layer.value(forKey: "affineTransform"))が、図形の中央を中心に行われるようになります 

3. (0.5, 0.5) に設定するメリット

ContainerLayer で self.anchorPoint = CGPoint(x: 0.5, y: 0.5) としているのは、以下の操作を直感的にするためです。

  • 回転: 図形がその場(中央)でくるくる回ります。もし (0,0) だと、左下角を中心に回ってしまいます。
  • 拡大縮小: 中央から外側に向かって大きくなります。
  • 配置: マウスでクリックした位置(position)に、図形の真ん中が吸い付くように配置できます。

まとめ

  • position は「キャンバス上のどこに置くか」という外側の設定。
  • anchorPoint は「図形のどこを基準にするか」という内側の設定。
  • ご提示のコードの計算式は、**「図形の中央を基準点として、親の position と一致させる」**ための正しい補正処理です。

CALayer から CGSize を取得

補足:直接 width/height を取る

CGRect の拡張機能により、.size を挟まずに直接取得することも可能です(読み取り専用)。

undoManager 登録例

CanvasNSView は 自動的に UndoManager を持ちます。

Undo 登録関数

Stroke を削除する処理(Undo 側)

Redo 用の追加処理

外部から undoManager を使う

未確認コード

First Responder に送る

SwiftUI から Undo を発火させる

ハマりがちなこと

座標系を混同して計算すると色々合わない

  • ローカル座標系(.bouns)
  • ビュー座標系 (.position, ,frame)

NSViewRepresentable

  • updateNSView() ここでルートレイヤーの更新があるとその上に被せているレイヤーの位置が原点に合わせられる、対策:条件フラグ等で回避

コメント

タイトルとURLをコピーしました