import Foundation import UIKit private func a(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat { return 1.0 - 3.0 * a2 + 3.0 * a1 } private func b(_ a1: CGFloat, _ a2: CGFloat) -> CGFloat { return 3.0 * a2 - 6.0 * a1 } private func c(_ a1: CGFloat) -> CGFloat { return 3.0 * a1 } private func calcBezier(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat { return ((a(a1, a2)*t + b(a1, a2))*t + c(a1)) * t } private func calcSlope(_ t: CGFloat, _ a1: CGFloat, _ a2: CGFloat) -> CGFloat { return 3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1) } private func getTForX(_ x: CGFloat, _ x1: CGFloat, _ x2: CGFloat) -> CGFloat { var t = x var i = 0 while i < 4 { let currentSlope = calcSlope(t, x1, x2) if currentSlope == 0.0 { return t } else { let currentX = calcBezier(t, x1, x2) - x t -= currentX / currentSlope } i += 1 } return t } private func bezierPoint(_ x1: CGFloat, _ y1: CGFloat, _ x2: CGFloat, _ y2: CGFloat, _ x: CGFloat) -> CGFloat { var value = calcBezier(getTForX(x, x1, x2), y1, y2) if value >= 0.997 { value = 1.0 } return value } /// Bezier control points for displacement easing curve public struct DisplacementBezier { var x1: CGFloat var y1: CGFloat var x2: CGFloat var y2: CGFloat public init(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) { self.x1 = x1 self.y1 = y1 self.x2 = x2 self.y2 = y2 } } /// Computes signed distance from a point to the edge of a rounded rectangle. /// Returns negative inside, zero on edge, positive outside. /// All values in points. public func roundedRectSDF(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> CGFloat { // Center the point (SDF formula assumes center at origin) let px = x - width / 2 let py = y - height / 2 // Half extents of the box let bx = width / 2 let by = height / 2 // Standard rounded box SDF (Inigo Quilez formula) let qx = abs(px) - bx + cornerRadius let qy = abs(py) - by + cornerRadius let outsideDist = hypot(max(qx, 0), max(qy, 0)) let insideDist = min(max(qx, qy), 0) return outsideDist + insideDist - cornerRadius } /// Computes the gradient (outward normal) of the rounded rect SDF. /// Returns normalized direction perpendicular to the nearest edge point. public func roundedRectGradient(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, cornerRadius: CGFloat) -> (nx: CGFloat, ny: CGFloat) { // Center the point let px = x - width / 2 let py = y - height / 2 // Half extents let bx = width / 2 let by = height / 2 // q values from SDF formula let qx = abs(px) - bx + cornerRadius let qy = abs(py) - by + cornerRadius var nx: CGFloat = 0 var ny: CGFloat = 0 if qx > 0 && qy > 0 { // Corner region - normal points radially from corner arc center let d = hypot(qx, qy) if d > 0 { nx = qx / d ny = qy / d } } else if qx > qy { // Nearest point is on vertical edge (left or right) nx = 1 ny = 0 } else { // Nearest point is on horizontal edge (top or bottom) nx = 0 ny = 1 } // Restore sign based on which side of center we're on if px < 0 { nx = -nx } if py < 0 { ny = -ny } return (nx, ny) } /// Generates a displacement map image as a signed distance field from rounded rect edges. /// - edgeDistance: The distance (in points) over which displacement is applied /// - R channel: X displacement (127 = neutral, 0 = max left, 255 = max right) /// - G channel: Y displacement (127 = neutral, 0 = max up, 255 = max down) /// - B channel: Unused (always 0) /// Displacement is maximum at the edge and fades linearly to zero at edgeDistance. /// Actual displacement magnitude is applied when sampling the map. public func generateDisplacementMap(size: CGSize, cornerRadius: CGFloat, edgeDistance: CGFloat, scale: CGFloat) -> CGImage? { let width = Int(size.width * scale) let height = Int(size.height * scale) // Clamp corner radius let maxCornerRadius = min(size.width, size.height) / 2.0 let clampedRadius = min(cornerRadius, maxCornerRadius) // Create bitmap context var pixelData = [UInt8](repeating: 0, count: width * height * 4) for py in 0 ..< height { for px in 0 ..< width { // Convert pixel to point coordinates let x = CGFloat(px) / scale let y = CGFloat(py) / scale // Get signed distance (negative inside, positive outside) let sdf = roundedRectSDF(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) // Get gradient (outward normal direction) let (nx, ny) = roundedRectGradient(x: x, y: y, width: size.width, height: size.height, cornerRadius: clampedRadius) // Inward normal (content moves away from edge, toward center) let inwardX = -nx let inwardY = -ny // Distance from edge (positive inside the shape) let distFromEdge = -sdf // Weight: 1 at edge, 0 at edgeDistance (linear falloff) let weight = max(0, min(1, 1.0 - distFromEdge / edgeDistance)) // Displacement modulated by distance from edge let displacementX = inwardX * weight let displacementY = inwardY * weight // Encode in R/G: 127 = neutral, map -1..1 to 0..254 let r = UInt8(max(0, min(255, Int(127 + displacementX * 127)))) let g = UInt8(max(0, min(255, Int(127 + displacementY * 127)))) let idx = (py * width + px) * 4 pixelData[idx + 0] = r // X displacement pixelData[idx + 1] = g // Y displacement pixelData[idx + 2] = 0 // Unused pixelData[idx + 3] = 255 // A } } let colorSpace = CGColorSpaceCreateDeviceRGB() guard let context = CGContext( data: &pixelData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return nil } return context.makeImage() } /// Samples displacement from a displacement map with bilinear interpolation and bezier easing /// - Parameters: /// - x, y: Coordinates in the displacement map's pixel space /// - pixels: Pointer to displacement map pixel data /// - width, height: Displacement map dimensions /// - bytesPerRow, bytesPerPixel: Displacement map layout /// - bezier: Bezier control points for easing curve /// - Returns: Displacement (dx, dy) in range -1..1 with bezier easing applied public func sampleDisplacement( x: CGFloat, y: CGFloat, pixels: UnsafePointer, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, bezier: DisplacementBezier ) -> (dx: CGFloat, dy: CGFloat) { let clampedX = max(0, min(CGFloat(width - 1), x)) let clampedY = max(0, min(CGFloat(height - 1), y)) let x0 = Int(clampedX) let y0 = Int(clampedY) let x1 = min(x0 + 1, width - 1) let y1 = min(y0 + 1, height - 1) let fx = clampedX - CGFloat(x0) let fy = clampedY - CGFloat(y0) func sample(_ sx: Int, _ sy: Int) -> (r: CGFloat, g: CGFloat) { let offset = sy * bytesPerRow + sx * bytesPerPixel return (CGFloat(pixels[offset + 0]), CGFloat(pixels[offset + 1])) } let c00 = sample(x0, y0) let c10 = sample(x1, y0) let c01 = sample(x0, y1) let c11 = sample(x1, y1) let r = (c00.r * (1 - fx) + c10.r * fx) * (1 - fy) + (c01.r * (1 - fx) + c11.r * fx) * fy let g = (c00.g * (1 - fx) + c10.g * fx) * (1 - fy) + (c01.g * (1 - fx) + c11.g * fx) * fy // Decode: 127 = neutral, map 0..254 to -1..1 var dx = (r - 127.0) / 127.0 var dy = (g - 127.0) / 127.0 // Apply bezier easing to vector magnitude, preserving direction let mag = hypot(dx, dy) if mag > 0 { let newMag = bezierPoint(bezier.x1, bezier.y1, bezier.x2, bezier.y2, mag) let scale = newMag / mag dx *= scale dy *= scale } return (dx, dy) } /// Generates a glass mesh with corner-aware topology. /// - 4 radial corner wedges sampled in polar space /// - 4 edge strips aligned with the rectangle sides /// - 1 center patch /// Corner/edge seams share the same coordinates (but do not reuse vertices) so /// the neighbouring faces fit perfectly without T-junctions. public func generateGlassMeshFromDisplacementMap( size: CGSize, cornerRadius: CGFloat, displacementMap: CGImage, displacementMagnitudeU: CGFloat, displacementMagnitudeV: CGFloat, cornerResolution: Int, outerEdgeDistance: CGFloat, bezier: DisplacementBezier, generateWireframe: Bool = false ) -> (mesh: MeshTransform, wireframe: CGPath?) { guard let dispDataProvider = displacementMap.dataProvider, let dispData = dispDataProvider.data, let dispPixels = CFDataGetBytePtr(dispData) else { return (mesh: MeshTransform(), wireframe: nil) } let dispWidth = displacementMap.width let dispHeight = displacementMap.height let dispBytesPerRow = displacementMap.bytesPerRow let dispBytesPerPixel = displacementMap.bitsPerPixel / 8 let clampedRadius = min(cornerRadius, min(size.width, size.height) / 2) let transform = MeshTransform() var wireframe: CGMutablePath? if generateWireframe { wireframe = CGMutablePath() } // Debug flags let debugNoDisplacement = false let debugLogCorner = false // Inset the mesh slightly (1 pixel) to clear the clip mask let insetPoints = -1.0 let usableWidth = max(1.0, size.width - insetPoints * 2) let usableHeight = max(1.0, size.height - insetPoints * 2) let insetUOffset = insetPoints / size.width let insetVOffset = insetPoints / size.height let usableUNorm = usableWidth / size.width let usableVNorm = usableHeight / size.height // Helper to sample displacement and create vertex func makeVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> (vertex: MeshTransform.Vertex, point: CGPoint) { let mappedU = insetUOffset + u * usableUNorm let mappedV = insetVOffset + v * usableVNorm let fromX: CGFloat let fromY: CGFloat if debugNoDisplacement { fromX = mappedU fromY = mappedV } else { let (dispX, dispY) = sampleDisplacement( x: mappedU * CGFloat(dispWidth - 1), y: mappedV * CGFloat(dispHeight - 1), pixels: dispPixels, width: dispWidth, height: dispHeight, bytesPerRow: dispBytesPerRow, bytesPerPixel: dispBytesPerPixel, bezier: bezier ) // Slight boost near the edge to emphasize the outer strip (rounded-corner aware) let worldX = insetPoints + u * usableWidth let worldY = insetPoints + v * usableHeight let sdf = roundedRectSDF(x: worldX, y: worldY, width: size.width, height: size.height, cornerRadius: clampedRadius) let distToEdge = max(0.0, -sdf) // distance inside the rounded rect to the edge let edgeBand = max(0.0, outerEdgeDistance) let edgeBoostGain: CGFloat = 0.5 // up to +50% displacement at the edge, fades inside let edgeBoost: CGFloat if edgeBand > 0 { let t = max(0.0, min(1.0, (edgeBand - distToEdge) / edgeBand)) let eased = t * t * (3 - 2 * t) // smoothstep edgeBoost = 1.0 + eased * edgeBoostGain } else { edgeBoost = 1.0 } fromX = max(0.0, min(1.0, mappedU + dispX * displacementMagnitudeU * edgeBoost)) fromY = max(0.0, min(1.0, mappedV + dispY * displacementMagnitudeV * edgeBoost)) } let vertex = MeshTransform.Vertex(from: CGPoint(x: fromX, y: fromY), to: MeshTransform.Point3D(x: mappedU, y: mappedV, z: depth)) return (vertex, CGPoint(x: mappedU * size.width, y: mappedV * size.height)) } var vertexIndex = 0 var vertexPoints: [CGPoint] = [] func addVertex(u: CGFloat, v: CGFloat, depth: CGFloat = 0) -> Int { let (vertex, point) = makeVertex(u: u, v: v, depth: depth) transform.add(vertex) vertexPoints.append(point) let idx = vertexIndex vertexIndex += 1 return idx } func addVertex(point: CGPoint, depth: CGFloat = 0) -> Int { let u = point.x / size.width let v = point.y / size.height return addVertex(u: u, v: v, depth: depth) } func addQuadFace(_ i0: Int, _ i1: Int, _ i2: Int, _ i3: Int) { let p0 = vertexPoints[i0] let p1 = vertexPoints[i1] let p2 = vertexPoints[i2] let p3 = vertexPoints[i3] let sdf0 = roundedRectSDF(x: p0.x, y: p0.y, width: size.width, height: size.height, cornerRadius: clampedRadius) let sdf1 = roundedRectSDF(x: p1.x, y: p1.y, width: size.width, height: size.height, cornerRadius: clampedRadius) let sdf2 = roundedRectSDF(x: p2.x, y: p2.y, width: size.width, height: size.height, cornerRadius: clampedRadius) let sdf3 = roundedRectSDF(x: p3.x, y: p3.y, width: size.width, height: size.height, cornerRadius: clampedRadius) if sdf0 > 0 && sdf1 > 0 && sdf2 > 0 && sdf3 > 0 { return } transform.add(MeshTransform.Face(indices: (UInt32(i0), UInt32(i1), UInt32(i2), UInt32(i3)), w: (0.0, 0.0, 0.0, 0.0))) if let wireframe { wireframe.move(to: p0) wireframe.addLine(to: p1) wireframe.addLine(to: p2) wireframe.addLine(to: p3) wireframe.closeSubpath() } } // Utility to build a grid of vertices from 2D points and emit quads func buildGrid(points: [[CGPoint]]) { guard !points.isEmpty else { return } var indexGrid: [[Int]] = [] for row in points { var rowIndices: [Int] = [] for point in row { rowIndices.append(addVertex(point: point)) } indexGrid.append(rowIndices) } let numRows = indexGrid.count - 1 let numCols = indexGrid.first!.count - 1 for row in 0.. [CGFloat] { guard count > 0, maxRadius > 0 else { return [0, 1] } let bandNorm = max(0, min(1, band / maxRadius)) // Evenly distribute inner rings up to (1 - bandNorm), then insert the outer strip edge and 1.0. let innerSegments = max(1, count - 1) let innerMax = max(0, 1 - bandNorm) var factors: [CGFloat] = (0...innerSegments).map { i in innerMax * CGFloat(i) / CGFloat(innerSegments) } func appendUnique(_ value: CGFloat) { if let last = factors.last, abs(last - value) < 1e-4 { return } factors.append(value) } appendUnique(innerMax) appendUnique(1.0) return factors } let depthFactors = depthFactorsWithOuterBand(count: radialSteps, band: outerEdgeDistance, maxRadius: clampedRadius) // 0...1 let angularFactors = (0...angularSteps).map { CGFloat($0) / CGFloat(angularSteps) } // 0...1 // Edge segmentation along the long axes; even spacing let horizontalSegments = max(2, cornerResolution / 2 + 1) let verticalSegments = max(2, cornerResolution / 2 + 1) func linearPositions(count: Int, start: CGFloat, end: CGFloat) -> [CGFloat] { return (0...count).map { i in let t = CGFloat(i) / CGFloat(count) return start + (end - start) * t } } // Shared tangential coordinates for strips/center let topXPositions: [CGFloat] = linearPositions( count: horizontalSegments, start: clampedRadius, end: width - clampedRadius ) let sideYPositions: [CGFloat] = linearPositions( count: verticalSegments, start: clampedRadius, end: height - clampedRadius ) // Shared depth coordinates (outer -> inner) so seams line up without T-junctions let outerToInner = depthFactors.reversed() let topYPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius let bottomYPositions: [CGFloat] = depthFactors.map { height - clampedRadius + clampedRadius * $0 } // (h-r) ... h let leftXPositions: [CGFloat] = outerToInner.map { clampedRadius * (1 - $0) } // 0 ... radius let rightXPositions: [CGFloat] = depthFactors.map { width - clampedRadius + clampedRadius * $0 } // (w-r) ... w // Corner wedges in polar space with an explicit center fan to avoid zero-area quads func buildCorner(center: CGPoint, startAngle: CGFloat, endAngle: CGFloat) { let ringRadials = outerToInner.filter { $0 > 0 } guard !ringRadials.isEmpty else { return } func formatVertex(_ idx: Int) -> String { let p = vertexPoints[idx] return "\(idx)=\(String(format: "(%.2f, %.2f)", p.x, p.y))" } // Generate ring vertices from outer arc toward the center point var ringIndices: [[Int]] = [] for radial in ringRadials { let r = clampedRadius * radial var row: [Int] = [] for t in angularFactors { let angle = startAngle + (endAngle - startAngle) * t let x = center.x + r * cos(angle) let y = center.y + r * sin(angle) row.append(addVertex(point: CGPoint(x: x, y: y))) } ringIndices.append(row) } // Quad rings between concentric samples for r in 0..<(ringIndices.count - 1) { let outerRing = ringIndices[r] let innerRing = ringIndices[r + 1] for i in 0..<(outerRing.count - 1) { addQuadFace( outerRing[i], outerRing[i + 1], innerRing[i + 1], innerRing[i] ) } } // Final collapse: merge two wedge slices into one quad anchored at the center. // Each quad spans a double-width wedge: center -> v0 -> v1 -> v2 (contiguous along the arc). if let innermostRing = ringIndices.last { let ringSegments = innermostRing.count - 1 // last point is the arc end (not wrapped) guard ringSegments >= 2 else { return } if debugLogCorner { let formatted = innermostRing.map { formatVertex($0) }.joined(separator: ", ") print("Corner collapse ringSegments=\(ringSegments) stride=2 angularSteps=\(angularSteps)") print("Innermost ring vertices: \(formatted)") } let centerAnchor = addVertex(point: center, depth: -0.02) let stride = 2 // Each quad covers two arc segments: (vi, vi+1) and (vi+1, vi+2) var i = 0 while i + 2 <= ringSegments { let v0 = innermostRing[i] let v1 = innermostRing[i + 1] let v2 = innermostRing[i + 2] if debugLogCorner { print("Quad indices: [\(centerAnchor), \(v0), \(v1), \(v2)]") } addQuadFace( centerAnchor, v0, v1, v2 ) i += stride } // Safety: if an odd segment remains, cap it with a final quad if i < ringSegments { let v0 = innermostRing[ringSegments - 1] let v1 = innermostRing[ringSegments] let v2 = innermostRing[ringSegments] // duplicate to keep quad valid if debugLogCorner { print("Quad indices (odd tail): [\(centerAnchor), \(v0), \(v1), \(v2)]") } addQuadFace(centerAnchor, v0, v1, v2) } } } // Edge strips func buildStrip(xPositions: [CGFloat], yPositions: [CGFloat]) { var points: [[CGPoint]] = [] for y in yPositions { let row = xPositions.map { CGPoint(x: $0, y: y) } points.append(row) } buildGrid(points: points) } // Top / bottom strips buildStrip(xPositions: topXPositions, yPositions: topYPositions) buildStrip(xPositions: topXPositions, yPositions: bottomYPositions) // Left / right strips buildStrip(xPositions: leftXPositions, yPositions: sideYPositions) buildStrip(xPositions: rightXPositions, yPositions: sideYPositions) // Center patch uses the same tangential sampling to meet edges cleanly buildStrip(xPositions: topXPositions, yPositions: sideYPositions) // Corners (angles chosen to keep columns increasing along +x) buildCorner( center: CGPoint(x: clampedRadius, y: clampedRadius), startAngle: .pi, endAngle: 1.5 * .pi ) buildCorner( center: CGPoint(x: width - clampedRadius, y: clampedRadius), startAngle: 1.5 * .pi, endAngle: 2 * .pi ) buildCorner( center: CGPoint(x: width - clampedRadius, y: height - clampedRadius), startAngle: .pi / 2, endAngle: 0 ) buildCorner( center: CGPoint(x: clampedRadius, y: height - clampedRadius), startAngle: .pi, endAngle: .pi / 2 ) return (mesh: transform, wireframe: wireframe) }