開始進行此作業後,找了找適合的卡通角色,本來嘗試做長髮公主、珍珠美人魚…但都因為圖形太複雜,失敗告終。最後選擇了哆啦A夢,因為他圓滾滾的,感覺比較好實現。

製作過程

一開始,我先請 AI 幫我分析哆啦A夢的視覺結構,並生成了一段基礎的 SVG 代碼。AI 很快就抓到了頭部比例、眼睛位置等大框架。接著,我再請它將這些邏輯轉化為 SwiftUI 的 Circle 和 Path。

這部分讓我最滿意的是,AI 幫我省去了手算座標的繁瑣過程,尤其是那些對稱的計算(像是左右手的擺放位置),幾乎是瞬間完成,這讓我有更多時間專注在微調細節。不過還是有許多地方需要人工調整!

AI 產出的初版雖然像,但總覺得少了點靈魂。以下是我花最多時間的地方: 眼神的細節:AI 給的眼睛比例有點太過機械化。我手動微調了 Ellipse 的 rx 和 ry,並重新定位了瞳孔中的高光,讓它看起來更有神。 紅鼻子的部分: 我特別用了 RadialGradient。AI 給出的漸層色階太跳了,我微調了 startRadius 和顏色比例,才做出了那種像乒乓球一樣、圓潤又有光澤的立體效果。

這次的製作讓我體會到,AI 是一個極強的輔助工具,它能處理掉 70% 的重複性勞動(如基礎座標、語法結構),但剩下的 30% ,關於審美、細節與情感,還是需要人類開發者的修正微調。

最終成果

None

程式碼

//
//  ContentView.swift
//  figure
//
//  Created by 楊哲鈞Owen on 2026/3/17.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        ZStack {
            LinearGradient(
                colors: [
                    Color(red: 0.78, green: 0.92, blue: 0.99),
                    Color(red: 0.95, green: 0.97, blue: 1.00),
                    Color(red: 0.83, green: 0.91, blue: 0.99)
                ],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()

            ScrollView {
                VStack(spacing: 28) {
                    titleSection
                    doraemonSection
                }
                .padding(.horizontal, 24)
                .padding(.vertical, 32)
            }
        }
    }

    private var titleSection: some View {
        Text("Doraemon")
            .font(.system(size: 40, weight: .bold, design: .rounded))
            .foregroundStyle(Color(red: 0.08, green: 0.20, blue: 0.36))
    }

    private var doraemonSection: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 32, style: .continuous)
                .fill(Color.white.opacity(0.72))
                .frame(height: 520)
                .shadow(color: .blue.opacity(0.12), radius: 24, x: 0, y: 14)

            DoraemonView()
        }
    }
}

// MARK: - 多啦A夢零件
struct DoraemonView: View {
    var body: some View {
        ZStack {
            let blue = Color(red: 0.0, green: 0.545, blue: 0.89)

            // 左手手臂
            Capsule()
                .fill(blue)
                .frame(width: 18, height: 72)
                .shadow(color: .black.opacity(0.10), radius: 3, x: 0, y: 2)
                .overlay {
                    Capsule()
                        .stroke(.black, lineWidth: 1)
                }
                .rotationEffect(.degrees(-130))
                .position(x: 60, y: 166)

            // 左手手掌
            Circle()
                .fill(.white)
                .frame(width: 25, height: 25)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 30, y: 191)

            // 右手手臂
            Capsule()
                .fill(blue)
                .frame(width: 18, height: 72)
                .shadow(color: .black.opacity(0.10), radius: 3, x: 0, y: 2)
                .overlay {
                    Capsule()
                        .stroke(.black, lineWidth: 1)
                }
                .rotationEffect(.degrees(130))
                .position(x: 140, y: 166)

            // 右手手掌
            Circle()
                .fill(.white)
                .frame(width: 25, height: 25)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 168, y: 187)

            // 左腳
            Capsule()
                .fill(blue)
                .frame(width: 16, height: 34)
                .shadow(color: .black.opacity(0.10), radius: 3, x: 0, y: 2)
                .overlay {
                    Capsule()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 84, y: 213)

            // 左腳腳掌
            Ellipse()
                .fill(.white)
                .frame(width: 34, height: 18)
                .shadow(color: .black.opacity(0.08), radius: 3, x: 0, y: 2)
                .overlay {
                    Ellipse()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 79, y: 226)

            // 右腳
            Capsule()
                .fill(blue)
                .frame(width: 16, height: 34)
                .shadow(color: .black.opacity(0.10), radius: 3, x: 0, y: 2)
                .overlay {
                    Capsule()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 116, y: 213)

            // 右腳腳掌
            Ellipse()
                .fill(.white)
                .frame(width: 34, height: 18)
                .shadow(color: .black.opacity(0.08), radius: 3, x: 0, y: 2)
                .overlay {
                    Ellipse()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 121, y: 226)

            // 身體外圈
            Circle()
                .fill(blue)
                .frame(width: 90, height: 90)
                .shadow(color: .black.opacity(0.12), radius: 8, x: 0, y: 5)
                .overlay {
                    Circle()
                        .fill(
                            LinearGradient(
                                colors: [.white.opacity(0.18), .clear],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing
                            )
                        )
                }
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1.5)
                }
                .position(x: 100, y: 180)

            // 肚子
            Circle()
                .fill(.white)
                .frame(width: 70, height: 70)
                .shadow(color: .black.opacity(0.08), radius: 4, x: 0, y: 2)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 100, y: 180)

            // 肚子口袋弧線
            Path { path in
                path.move(to: CGPoint(x: 75, y: 180))
                path.addArc(
                    center: CGPoint(x: 100, y: 180),
                    radius: 25,
                    startAngle: .degrees(0),
                    endAngle: .degrees(180),
                    clockwise: false
                )
                path.closeSubpath()
            }
            .stroke(Color.gray.opacity(0.85), lineWidth: 1)

            // 頭部外圈
            Circle()
                .fill(blue)
                .frame(width: 150, height: 150)
                .shadow(color: .black.opacity(0.14), radius: 12, x: 0, y: 7)
                .overlay {
                    Circle()
                        .fill(
                            LinearGradient(
                                colors: [.white.opacity(0.22), .clear],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing
                            )
                        )
                }
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1.5)
                }
                .position(x: 100, y: 90)

            // 臉部白色區域
            Circle()
                .fill(.white)
                .frame(width: 124, height: 124)
                .shadow(color: .black.opacity(0.08), radius: 4, x: 0, y: 2)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 100, y: 100)

            // 左眼外框
            Ellipse()
                .fill(.white)
                .frame(width: 36, height: 44)
                .overlay {
                    Ellipse()
                        .stroke(.black, lineWidth: 1.5)
                }
                .position(x: 82, y: 65)

            // 右眼外框
            Ellipse()
                .fill(.white)
                .frame(width: 36, height: 44)
                .overlay {
                    Ellipse()
                        .stroke(.black, lineWidth: 1.5)
                }
                .position(x: 118, y: 65)

            // 左眼瞳孔
            Circle()
                .fill(.black)
                .frame(width: 8, height: 8)
                .position(x: 88, y: 72)

            // 右眼瞳孔
            Circle()
                .fill(.black)
                .frame(width: 8, height: 8)
                .position(x: 112, y: 72)

            // 左眼高光
            Circle()
                .fill(.white.opacity(0.9))
                .frame(width: 3, height: 3)
                .position(x: 89, y: 70)

            // 右眼高光
            Circle()
                .fill(.white.opacity(0.9))
                .frame(width: 3, height: 3)
                .position(x: 113, y: 70)

            // 鼻子
            Circle()
                .fill(
                    RadialGradient(
                        colors: [Color(red: 1.0, green: 0.3, blue: 0.3), Color(red: 0.7, green: 0.0, blue: 0.0)],
                        center: .center,
                        startRadius: 2,
                        endRadius: 9
                    )
                )
                .frame(width: 18, height: 18)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 0.5)
                }
                .position(x: 100, y: 90)

            // 鼻子高光
            Circle()
                .fill(.white.opacity(0.6))
                .frame(width: 5, height: 5)
                .position(x: 97, y: 87)

            // 鼻子到嘴巴的中線
            Path { path in
                path.move(to: CGPoint(x: 100, y: 99))
                path.addLine(to: CGPoint(x: 100, y: 135))
            }
            .stroke(.black, lineWidth: 1.5)

            // 嘴巴弧線
            Path { path in
                path.move(to: CGPoint(x: 60, y: 125))
                path.addQuadCurve(to: CGPoint(x: 140, y: 125), control: CGPoint(x: 100, y: 160))
            }
            .stroke(.black, lineWidth: 1.5)

            // 左右鬍鬚
            DoraemonWhiskerSet()

            // 紅色項圈
            Path { path in
                path.move(to: CGPoint(x: 63, y: 158))
                path.addQuadCurve(to: CGPoint(x: 137, y: 158), control: CGPoint(x: 100, y: 175))
            }
            .stroke(
                Color.red,
                style: StrokeStyle(lineWidth: 8, lineCap: .round)
            )

            // 鈴鐺外圈
            Circle()
                .fill(
                    RadialGradient(
                        colors: [Color(red: 0.98, green: 0.89, blue: 0.55), Color(red: 0.83, green: 0.69, blue: 0.22)],
                        center: .center,
                        startRadius: 2,
                        endRadius: 10
                    )
                )
                .frame(width: 20, height: 20)
                .shadow(color: .black.opacity(0.12), radius: 3, x: 0, y: 2)
                .overlay {
                    Circle()
                        .stroke(.black, lineWidth: 1)
                }
                .position(x: 100, y: 170)

            // 鈴鐺橫線
            Path { path in
                path.move(to: CGPoint(x: 91, y: 167))
                path.addLine(to: CGPoint(x: 109, y: 167))
            }
            .stroke(.black, lineWidth: 1)

            // 鈴鐺中心小孔
            Circle()
                .fill(Color.gray.opacity(0.9))
                .frame(width: 5, height: 5)
                .position(x: 100, y: 174)

            // 鈴鐺垂直線
            Path { path in
                path.move(to: CGPoint(x: 100, y: 174))
                path.addLine(to: CGPoint(x: 100, y: 180))
            }
            .stroke(.black, lineWidth: 1)
        }
        .frame(width: 200, height: 230)
        .scaleEffect(1.48)
    }
}

private struct DoraemonWhiskerSet: View {
    var body: some View {
        Path { path in
            // 右上鬍鬚
            path.move(to: CGPoint(x: 120, y: 105))
            path.addLine(to: CGPoint(x: 160, y: 95))

            // 右中鬍鬚
            path.move(to: CGPoint(x: 120, y: 115))
            path.addLine(to: CGPoint(x: 165, y: 115))

            // 右下鬍鬚
            path.move(to: CGPoint(x: 120, y: 125))
            path.addLine(to: CGPoint(x: 160, y: 135))

            // 左上鬍鬚
            path.move(to: CGPoint(x: 80, y: 105))
            path.addLine(to: CGPoint(x: 40, y: 95))

            // 左中鬍鬚
            path.move(to: CGPoint(x: 80, y: 115))
            path.addLine(to: CGPoint(x: 35, y: 115))

            // 左下鬍鬚
            path.move(to: CGPoint(x: 80, y: 125))
            path.addLine(to: CGPoint(x: 40, y: 135))
        }
        .stroke(.black, lineWidth: 1.5)
    }
}

#Preview {
    ContentView()
}