240 lines
7.2 KiB
Swift
240 lines
7.2 KiB
Swift
// GPPanels.swift
|
|
// SwiftUI panels for the Grease Pencil workspace.
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - Brush Panel
|
|
|
|
struct GPBrushPanel: View {
|
|
@State private var selectedBrush: GPBrushType = .draw
|
|
@State private var brushSize: Float = 25
|
|
@State private var brushStrength: Float = 1.0
|
|
@State private var brushColor: Color = .black
|
|
|
|
var body: some View {
|
|
VStack(spacing: 8) {
|
|
// Brush type selector
|
|
ForEach(GPBrushType.allCases) { brush in
|
|
Button {
|
|
selectedBrush = brush
|
|
} label: {
|
|
Image(systemName: brush.icon)
|
|
.font(.system(size: 16))
|
|
.frame(width: 36, height: 36)
|
|
.background(
|
|
selectedBrush == brush
|
|
? Color.orange.opacity(0.25)
|
|
: Color.clear,
|
|
in: RoundedRectangle(cornerRadius: 6)
|
|
)
|
|
}
|
|
.tint(selectedBrush == brush ? .orange : .secondary)
|
|
}
|
|
|
|
Divider().padding(.horizontal, 8)
|
|
|
|
// Color
|
|
ColorPicker("", selection: $brushColor)
|
|
.labelsHidden()
|
|
.frame(width: 36, height: 36)
|
|
|
|
Divider().padding(.horizontal, 8)
|
|
|
|
// Quick presets
|
|
VStack(spacing: 4) {
|
|
Text("Size")
|
|
.font(.system(size: 8))
|
|
.foregroundStyle(.secondary)
|
|
Text("\(Int(brushSize))")
|
|
.font(.system(size: 10, design: .monospaced))
|
|
.foregroundStyle(.primary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.vertical, 8)
|
|
.padding(.horizontal, 4)
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
}
|
|
|
|
enum GPBrushType: String, CaseIterable, Identifiable {
|
|
case draw = "Draw"
|
|
case fill = "Fill"
|
|
case erase = "Erase"
|
|
case tint = "Tint"
|
|
case smooth = "Smooth"
|
|
case grab = "Grab"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .draw: "pencil.tip"
|
|
case .fill: "paintbrush.fill"
|
|
case .erase: "eraser"
|
|
case .tint: "eyedropper"
|
|
case .smooth: "wand.and.stars"
|
|
case .grab: "hand.point.up.left"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Layer Panel
|
|
|
|
struct GPLayerPanel: View {
|
|
@State private var layers: [GPLayerInfo] = [
|
|
.init(name: "Fills", isVisible: true, isActive: false),
|
|
.init(name: "Lines", isVisible: true, isActive: true),
|
|
.init(name: "Sketch", isVisible: false, isActive: false),
|
|
]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Header
|
|
HStack {
|
|
Text("Layers")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Button {
|
|
let new = GPLayerInfo(
|
|
name: "Layer \(layers.count + 1)",
|
|
isVisible: true,
|
|
isActive: false
|
|
)
|
|
layers.insert(new, at: 0)
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
.font(.caption2)
|
|
}
|
|
.tint(.secondary)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 8)
|
|
|
|
Divider()
|
|
|
|
// Layer list
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
ForEach(layers) { layer in
|
|
GPLayerRow(layer: layer) {
|
|
// Select
|
|
for i in layers.indices {
|
|
layers[i].isActive = (layers[i].id == layer.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
}
|
|
|
|
struct GPLayerInfo: Identifiable {
|
|
let id = UUID()
|
|
var name: String
|
|
var isVisible: Bool
|
|
var isActive: Bool
|
|
}
|
|
|
|
struct GPLayerRow: View {
|
|
let layer: GPLayerInfo
|
|
let onSelect: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onSelect) {
|
|
HStack(spacing: 8) {
|
|
// Color swatch
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(layer.isActive ? Color.orange : Color.gray.opacity(0.3))
|
|
.frame(width: 4, height: 24)
|
|
|
|
Text(layer.name)
|
|
.font(.caption)
|
|
.foregroundStyle(layer.isActive ? .primary : .secondary)
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: layer.isVisible ? "eye" : "eye.slash")
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(layer.isActive ? Color.orange.opacity(0.08) : .clear)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
// MARK: - Onion Skin Panel
|
|
|
|
struct OnionSkinPanel: View {
|
|
@State private var enabled = true
|
|
@State private var framesBefore: Double = 3
|
|
@State private var framesAfter: Double = 1
|
|
@State private var opacity: Double = 0.5
|
|
@State private var colorBefore: Color = .red.opacity(0.3)
|
|
@State private var colorAfter: Color = .green.opacity(0.3)
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("Onion Skin")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Toggle("", isOn: $enabled)
|
|
.labelsHidden()
|
|
.scaleEffect(0.7)
|
|
}
|
|
.padding(.horizontal, 10)
|
|
.padding(.top, 8)
|
|
|
|
if enabled {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
SliderRow(label: "Before", value: $framesBefore, range: 0...10)
|
|
SliderRow(label: "After", value: $framesAfter, range: 0...5)
|
|
SliderRow(label: "Opacity", value: $opacity, range: 0...1)
|
|
|
|
HStack(spacing: 12) {
|
|
ColorPicker("Before", selection: $colorBefore)
|
|
.font(.caption2)
|
|
ColorPicker("After", selection: $colorAfter)
|
|
.font(.caption2)
|
|
}
|
|
}
|
|
.padding(.horizontal, 10)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.background(.ultraThinMaterial)
|
|
}
|
|
}
|
|
|
|
struct SliderRow: View {
|
|
let label: String
|
|
@Binding var value: Double
|
|
let range: ClosedRange<Double>
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
HStack {
|
|
Text(label)
|
|
.font(.system(size: 10))
|
|
.foregroundStyle(.secondary)
|
|
Spacer()
|
|
Text(String(format: range.upperBound > 1 ? "%.0f" : "%.2f", value))
|
|
.font(.system(size: 10, design: .monospaced))
|
|
}
|
|
Slider(value: $value, in: range)
|
|
.tint(.orange)
|
|
}
|
|
}
|
|
}
|