Code Sketch
dr babasaheb 3d yoi
Category: Programming
import java.awt._
import java.awt.event._
import java.awt.image.BufferedImage
import java.awt.image.DataBufferInt
import java.io.File
import javax.imageio.ImageIO
import javax.swing._
import scala.collection.mutable.ArrayBuffer
// ================================================================
// BABASAHEB 3D STATUE STUDIO
// A self-contained Scala + Swing procedural 3D statue viewer.
// No external libraries or copyrighted model files are required.
// It creates a stylized, colored 3D representation inspired by
// the widely used blue-suit, glasses, pointing-hand, Constitution
// book pose of Dr. B. R. Ambedkar.
// ================================================================
object Babasaheb3DStatue {
case class V(x: Double, y: Double, z: Double) {
def +(b: V): V = V(x + b.x, y + b.y, z + b.z)
def -(b: V): V = V(x - b.x, y - b.y, z - b.z)
def *(k: Double): V = V(x * k, y * k, z * k)
}
case class Tri(a: V, b: V, c: V, color: Color)
case class TV(x: Double, y: Double, z: Double)
case class SV(x: Double, y: Double, invZ: Double)
val triangles = ArrayBuffer[Tri]()
// Camera / viewer state.
var yaw = 0.0
var pitch = 0.02
var zoom = 9.4
val targetY = 2.65
var themeMode = 0
var showHalo = true
var showBook = true
var showStars = true
val backgroundTop = new Color(8, 14, 35)
val backgroundBottom = new Color(45, 27, 57)
val gold = new Color(255, 200, 72)
val classicBlue = new Color(25, 78, 190)
val skinClassic = new Color(190, 137, 105)
val darkClassic = new Color(22, 22, 27)
def clamp(v: Double, lo: Double, hi: Double): Double =
math.max(lo, math.min(hi, v))
def dot(a: V, b: V): Double = a.x * b.x + a.y * b.y + a.z * b.z
def cross(a: V, b: V): V = V(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x
)
def length(v: V): Double = math.sqrt(dot(v, v))
def norm(v: V): V = {
val n = length(v)
if (n < 0.000001) V(0, 1, 0) else V(v.x / n, v.y / n, v.z / n)
}
def recolor(role: String, c: Color): Color = {
if (themeMode == 0) c
else if (themeMode == 1) { // bronze sculpture
role match {
case "glasses" | "eye" | "dark" => new Color(45, 31, 25)
case "gold" => new Color(222, 167, 69)
case "base" => new Color(59, 47, 42)
case "background" => c
case _ => new Color(151, 91, 53)
}
} else if (themeMode == 2) { // golden tribute statue
role match {
case "glasses" | "eye" | "dark" => new Color(58, 39, 19)
case "base" => new Color(22, 39, 83)
case "background" => c
case _ => new Color(236, 177, 43)
}
} else { // marble
role match {
case "glasses" | "eye" | "dark" => new Color(51, 55, 66)
case "gold" => new Color(170, 139, 76)
case "base" => new Color(62, 72, 91)
case "background" => c
case _ => new Color(220, 222, 224)
}
}
}
def scaleColor(c: Color, factor: Double): Color = new Color(
clamp(c.getRed * factor, 0, 255).toInt,
clamp(c.getGreen * factor, 0, 255).toInt,
clamp(c.getBlue * factor, 0, 255).toInt
)
def addTri(a: V, b: V, c: V, base: Color): Unit = {
val n = norm(cross(b - a, c - a))
val light = norm(V(-0.38, 0.82, 0.58))
val diffuse = math.abs(dot(n, light))
val factor = 0.47 + 0.53 * diffuse
triangles += Tri(a, b, c, scaleColor(base, factor))
}
def addBox(cx: Double, cy: Double, cz: Double,
sx: Double, sy: Double, sz: Double, color: Color): Unit = {
val x0 = cx - sx / 2; val x1 = cx + sx / 2
val y0 = cy - sy / 2; val y1 = cy + sy / 2
val z0 = cz - sz / 2; val z1 = cz + sz / 2
val v = Array(
V(x0,y0,z0), V(x1,y0,z0), V(x1,y1,z0), V(x0,y1,z0),
V(x0,y0,z1), V(x1,y0,z1), V(x1,y1,z1), V(x0,y1,z1)
)
val f = Array(
Array(0,1,2), Array(0,2,3), Array(4,7,6), Array(4,6,5),
Array(0,4,5), Array(0,5,1), Array(3,2,6), Array(3,6,7),
Array(0,3,7), Array(0,7,4), Array(1,5,6), Array(1,6,2)
)
f.foreach(k => addTri(v(k(0)), v(k(1)), v(k(2)), color))
}
def addEllipsoid(center: V, rx: Double, ry: Double, rz: Double,
color: Color, lat: Int = 14, lon: Int = 22): Unit = {
def point(i: Int, j: Int): V = {
val phi = math.Pi * i.toDouble / lat
val theta = 2.0 * math.Pi * j.toDouble / lon
V(center.x + rx * math.sin(phi) * math.cos(theta),
center.y + ry * math.cos(phi),
center.z + rz * math.sin(phi) * math.sin(theta))
}
for (i <- 0 until lat; j <- 0 until lon) {
val a = point(i, j)
val b = point(i + 1, j)
val c = point(i + 1, (j + 1) % lon)
val d = point(i, (j + 1) % lon)
addTri(a, b, c, color)
addTri(a, c, d, color)
}
}
// Cylinder / capsule-like limb between two 3D points.
def addCylinder(a: V, b: V, rA: Double, rB: Double,
color: Color, seg: Int = 12, caps: Boolean = true): Unit = {
val axis = norm(b - a)
val helper = if (math.abs(axis.y) < 0.88) V(0,1,0) else V(1,0,0)
val u = norm(cross(axis, helper))
val v = norm(cross(axis, u))
val ringA = (0 until seg).map { i =>
val t = 2.0 * math.Pi * i / seg
a + u * (math.cos(t) * rA) + v * (math.sin(t) * rA)
}.toVector
val ringB = (0 until seg).map { i =>
val t = 2.0 * math.Pi * i / seg
b + u * (math.cos(t) * rB) + v * (math.sin(t) * rB)
}.toVector
for (i <- 0 until seg) {
val j = (i + 1) % seg
addTri(ringA(i), ringA(j), ringB(j), color)
addTri(ringA(i), ringB(j), ringB(i), color)
if (caps) {
addTri(a, ringA(j), ringA(i), color)
addTri(b, ringB(i), ringB(j), color)
}
}
}
def addCircle(cx: Double, cy: Double, z: Double, radius: Double,
tube: Double, color: Color, seg: Int = 40): Unit = {
for (i <- 0 until seg) {
val t0 = 2.0 * math.Pi * i / seg
val t1 = 2.0 * math.Pi * (i + 1) / seg
val a = V(cx + radius * math.cos(t0), cy + radius * math.sin(t0), z)
val b = V(cx + radius * math.cos(t1), cy + radius * math.sin(t1), z)
addCylinder(a, b, tube, tube, color, 6, false)
}
}
def addTorusXY(cx: Double, cy: Double, cz: Double, major: Double,
tube: Double, color: Color, sides: Int = 72,
rings: Int = 8): Unit = {
def p(i: Int, j: Int): V = {
val t = 2.0 * math.Pi * i / sides
val u = 2.0 * math.Pi * j / rings
V(cx + (major + tube * math.cos(u)) * math.cos(t),
cy + (major + tube * math.cos(u)) * math.sin(t),
cz + tube * math.sin(u))
}
for (i <- 0 until sides; j <- 0 until rings) {
val a = p(i,j); val b = p((i+1)%sides,j)
val c = p((i+1)%sides,(j+1)%rings); val d = p(i,(j+1)%rings)
addTri(a,b,c,color); addTri(a,c,d,color)
}
}
def addGround(): Unit = {
val c = new Color(28, 34, 54)
val a = V(-24, -0.035, -22); val b = V(24, -0.035, -22)
val d = V(-24, -0.035, 22); val e = V(24, -0.035, 22)
addTri(a,d,e,c); addTri(a,e,b,c)
}
def C(role: String, color: Color): Color = recolor(role, color)
// --------------------------------------------------------------
// BUILD THE STATUE: face, glasses, pointing arm, book and suit.
// --------------------------------------------------------------
def buildScene(): Unit = {
triangles.clear()
addGround()
// Decorative halo stays behind the statue.
if (showHalo) {
addTorusXY(0, 2.82, -0.88, 2.23, 0.065, C("gold", gold), 72, 8)
addTorusXY(0, 2.82, -0.90, 2.05, 0.018, C("gold", new Color(220, 169, 60)), 72, 6)
for (i <- 0 until 28) {
val a = i * 2.0 * math.Pi / 28.0
val p1 = V(2.38 * math.cos(a), 2.82 + 2.38 * math.sin(a), -0.86)
val p2 = V(2.58 * math.cos(a), 2.82 + 2.58 * math.sin(a), -0.86)
addCylinder(p1, p2, 0.018, 0.012, C("gold", gold), 6, false)
}
}
// Gentle distant decorative lights.
if (showStars) {
val stars = Array(
(-3.0,4.45),(-2.8,2.1),(-2.45,3.7),(2.8,4.3),(3.0,2.8),
(2.6,1.25),(-2.5,1.25),(1.9,4.75),(-1.8,4.8),(3.25,3.65)
)
stars.foreach { s =>
addEllipsoid(V(s._1,s._2,-1.15),0.035,0.035,0.025,
C("gold",new Color(255, 219, 127)),8,10)
}
}
// Pedestal: layered circular 3D plinth.
addCylinder(V(0,0.02,0), V(0,0.24,0), 1.02, 1.02,
C("base",new Color(20,46,103)), 48, true)
addCylinder(V(0,0.24,0), V(0,0.53,0), 0.84, 0.84,
C("base",new Color(25,65,139)), 48, true)
addCylinder(V(0,0.53,0), V(0,0.80,0), 0.73, 0.68,
C("base",new Color(24,48,96)), 48, true)
addCylinder(V(0,0.79,0), V(0,0.84,0), 0.70, 0.70,
C("gold",new Color(218,174,72)), 48, true)
addBox(0,0.41,0.70,1.48,0.36,0.12,C("base",new Color(15,36,83)))
addBox(0,0.41,0.768,1.34,0.25,0.025,C("gold",new Color(197,151,58)))
addBox(0,0.41,0.786,1.28,0.20,0.018,C("base",new Color(19,41,89)))
// Shoes, trousers and polished black shoes.
addEllipsoid(V(-0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
addEllipsoid(V(0.23,0.91,0.10),0.22,0.12,0.34,C("dark",new Color(20,22,27)),12,20)
addBox(-0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))
addBox(0.23,0.99,0.07,0.26,0.08,0.35,C("dark",new Color(38,39,43)))
val pants = C("suit",new Color(24,70,163))
addCylinder(V(-0.23,0.98,0.00),V(-0.29,2.04,-0.01),0.19,0.23,pants,18,true)
addCylinder(V(0.23,0.98,0.00),V(0.29,2.04,-0.01),0.19,0.23,pants,18,true)
addEllipsoid(V(0,2.00,-0.01),0.43,0.30,0.29,pants,14,22)
addBox(-0.24,1.55,0.00,0.28,0.78,0.34,pants)
addBox(0.24,1.55,0.00,0.28,0.78,0.34,pants)
// Shirt and main suit jacket.
val suit = C("suit",classicBlue)
val shirt = C("shirt",new Color(241,238,226))
val skin = C("skin",skinClassic)
val tie = C("tie",new Color(188,27,47))
val hair = C("dark",new Color(25,24,27))
val jacketDark = scaleColor(suit,0.78)
addEllipsoid(V(0,2.72,-0.015),0.65,0.88,0.36,suit,20,28)
addEllipsoid(V(-0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
addEllipsoid(V(0.48,3.36,-0.015),0.29,0.29,0.31,suit,14,20)
addEllipsoid(V(0,3.46,-0.025),0.38,0.24,0.30,suit,14,20)
// White shirt panel on the front of the suit.
addEllipsoid(V(0,2.91,0.285),0.255,0.66,0.105,shirt,18,24)
addTri(V(-0.24,3.41,0.365),V(-0.04,3.12,0.405),V(-0.13,3.47,0.36),
C("shirt",new Color(247,245,236)))
addTri(V(0.24,3.41,0.365),V(0.04,3.12,0.405),V(0.13,3.47,0.36),
C("shirt",new Color(247,245,236)))
// Lapels, jacket seams and buttons.
addTri(V(-0.42,3.43,0.27),V(-0.12,3.02,0.40),V(-0.27,2.72,0.38),jacketDark)
addTri(V(0.42,3.43,0.27),V(0.12,3.02,0.40),V(0.27,2.72,0.38),jacketDark)
addTri(V(-0.10,3.42,0.39),V(-0.01,3.10,0.415),V(0.04,3.41,0.39),
C("shirt",new Color(245,241,230)))
addTri(V(-0.015,3.10,0.416),V(0.04,3.41,0.39),V(0.13,3.10,0.405),
C("shirt",new Color(245,241,230)))
addTri(V(-0.09,3.22,0.43),V(0,3.11,0.445),V(0.09,3.22,0.43),tie)
addTri(V(-0.085,3.17,0.44),V(0.085,3.17,0.44),V(0.025,2.43,0.44),tie)
addTri(V(-0.085,3.17,0.44),V(0.025,2.43,0.44),V(-0.025,2.43,0.44),tie)
for (yy <- Seq(2.55,2.78,3.00)) {
addEllipsoid(V(0.28,yy,0.345),0.028,0.028,0.022,C("gold",new Color(205,167,72)),8,10)
}
addBox(0.39,2.96,0.34,0.20,0.24,0.035,jacketDark)
addBox(0.39,2.96,0.364,0.15,0.18,0.018,suit)
// Right arm: extended to point outward (on the viewer's left).
addCylinder(V(-0.53,3.37,0.00),V(-0.97,3.68,0.08),0.20,0.175,suit,18,true)
addEllipsoid(V(-0.77,3.55,0.05),0.21,0.22,0.20,suit,14,18)
addCylinder(V(-0.97,3.68,0.08),V(-1.47,4.01,0.13),0.16,0.125,suit,18,true)
addCylinder(V(-1.45,4.00,0.13),V(-1.61,4.055,0.15),0.13,0.105,
C("shirt",new Color(238,234,222)),12,true)
addEllipsoid(V(-1.72,4.085,0.18),0.19,0.12,0.13,skin,14,18)
// Pointing index finger and curled remaining fingers.
addCylinder(V(-1.78,4.13,0.21),V(-2.22,4.32,0.22),0.047,0.027,skin,10,true)
addEllipsoid(V(-2.235,4.326,0.22),0.038,0.032,0.032,skin,8,10)
addCylinder(V(-1.82,4.07,0.22),V(-2.04,4.04,0.24),0.039,0.029,skin,10,true)
addCylinder(V(-1.80,4.04,0.22),V(-1.98,3.985,0.23),0.035,0.026,skin,10,true)
addCylinder(V(-1.75,4.02,0.22),V(-1.90,3.965,0.22),0.032,0.024,skin,10,true)
addCylinder(V(-1.67,4.04,0.22),V(-1.76,3.98,0.25),0.034,0.027,skin,10,true)
// Left arm: bent in toward the Constitution book.
addCylinder(V(0.51,3.34,0.00),V(0.78,2.96,0.22),0.19,0.16,suit,18,true)
addEllipsoid(V(0.68,3.13,0.12),0.19,0.22,0.19,suit,14,18)
addCylinder(V(0.78,2.96,0.22),V(0.59,2.64,0.42),0.15,0.12,suit,18,true)
addCylinder(V(0.59,2.66,0.42),V(0.54,2.58,0.51),0.12,0.09,
C("shirt",new Color(240,237,225)),12,true)
// Constitution book with cover, pages, gold border and page lines.
if (showBook) {
addBox(0.43,2.72,0.53,0.48,0.67,0.18,C("book",new Color(100,51,28)))
addBox(0.43,2.72,0.635,0.425,0.61,0.025,C("book",new Color(115,61,31)))
addBox(0.43,2.72,0.653,0.38,0.56,0.012,C("gold",new Color(212,174,83)))
addBox(0.43,2.72,0.661,0.35,0.53,0.01,C("book",new Color(99,47,25)))
for (yy <- Seq(2.57,2.62,2.67,2.72,2.77,2.82,2.87)) {
addBox(0.43,yy,0.67,0.25,0.008,0.006,C("gold",new Color(225,194,117)))
}
addBox(0.43,3.015,0.663,0.34,0.012,0.008,C("gold",new Color(231,202,132)))
}
// Neck, collar, head and ears.
addCylinder(V(0,3.38,0.00),V(0,3.78,0.00),0.19,0.17,skin,18,true)
addEllipsoid(V(0,3.70,0.06),0.27,0.14,0.23,shirt,14,20)
addEllipsoid(V(0,4.22,0.025),0.365,0.49,0.315,skin,24,32)
addEllipsoid(V(-0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)
addEllipsoid(V(0.355,4.19,0.015),0.075,0.12,0.07,skin,12,16)
// Neatly combed dark hair cap / side sections.
addEllipsoid(V(0,4.585,-0.005),0.355,0.145,0.30,hair,16,26)
addEllipsoid(V(-0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
addEllipsoid(V(0.30,4.43,0.005),0.09,0.19,0.26,hair,12,18)
addEllipsoid(V(0,4.50,0.275),0.26,0.045,0.055,hair,10,18)
// Eyes, pupils, brows, nose, mouth and iconic round glasses.
addEllipsoid(V(-0.135,4.265,0.308),0.061,0.043,0.027,
C("shirt",new Color(247,238,221)),10,14)
addEllipsoid(V(0.135,4.265,0.308),0.061,0.043,0.027,
C("shirt",new Color(247,238,221)),10,14)
addEllipsoid(V(-0.132,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
addEllipsoid(V(0.138,4.265,0.337),0.022,0.027,0.014,C("eye",new Color(28,25,24)),8,10)
addCylinder(V(-0.20,4.333,0.32),V(-0.07,4.342,0.326),0.018,0.016,hair,8,true)
addCylinder(V(0.07,4.342,0.326),V(0.20,4.333,0.32),0.016,0.018,hair,8,true)
val glasses = C("glasses",new Color(28,29,34))
addCircle(-0.135,4.265,0.36,0.105,0.014,glasses,32)
addCircle(0.135,4.265,0.36,0.105,0.014,glasses,32)
addCylinder(V(-0.03,4.27,0.36),V(0.03,4.27,0.36),0.014,0.014,glasses,8,false)
addCylinder(V(-0.24,4.28,0.34),V(-0.34,4.24,0.17),0.012,0.009,glasses,8,false)
addCylinder(V(0.24,4.28,0.34),V(0.34,4.24,0.17),0.012,0.009,glasses,8,false)
addEllipsoid(V(0,4.13,0.345),0.065,0.105,0.075,skin,12,16)
addEllipsoid(V(0,4.075,0.395),0.038,0.028,0.027,
C("skin",new Color(181,121,91)),10,12)
addCylinder(V(-0.085,3.995,0.316),V(0.085,3.995,0.316),0.013,0.013,
C("dark",new Color(102,54,47)),8,false)
addEllipsoid(V(0,3.945,0.302),0.085,0.035,0.035,
C("skin",new Color(177,117,91)),10,14)
// Small decorative lapel pin.
addEllipsoid(V(-0.34,3.18,0.367),0.045,0.045,0.02,
C("gold",new Color(248,203,76)),10,14)
}
// --------------------------------------------------------------
// SOFTWARE 3D RENDERER: near clipping + perspective + Z-buffer.
// --------------------------------------------------------------
val nearPlane = 0.18
def transform(p: V): TV = {
val cy = math.cos(yaw); val sy = math.sin(yaw)
val x1 = p.x * cy - p.z * sy
val z1 = p.x * sy + p.z * cy
val y1 = p.y - targetY
val cp = math.cos(pitch); val sp = math.sin(pitch)
val y2 = y1 * cp - z1 * sp
val z2 = y1 * sp + z1 * cp
TV(x1, y2, zoom - z2)
}
def clipNear(poly: scala.collection.immutable.List[TV]): scala.collection.immutable.List[TV] = {
if (poly.isEmpty) return poly
val out = ArrayBuffer[TV]()
var previous = poly.last
var previousInside = previous.z > nearPlane
poly.foreach { current =>
val currentInside = current.z > nearPlane
if (currentInside != previousInside) {
val den = current.z - previous.z
if (math.abs(den) > 0.0000001) {
val t = (nearPlane - previous.z) / den
out += TV(
previous.x + (current.x - previous.x) * t,
previous.y + (current.y - previous.y) * t,
nearPlane
)
}
}
if (currentInside) out += current
previous = current
previousInside = currentInside
}
out.toList
}
def project(p: TV, w: Int, h: Int): SV = {
val focal = math.min(w, h) * 1.03
SV(w / 2.0 + p.x * focal / p.z,
h / 2.0 - p.y * focal / p.z,
1.0 / p.z)
}
def rasterTriangle(imgPixels: Array[Int], zbuf: Array[Double],
w: Int, h: Int, a: SV, b: SV, c: SV, color: Color): Unit = {
val area = (b.y - c.y) * (a.x - c.x) + (c.x - b.x) * (a.y - c.y)
if (math.abs(area) < 0.00001) return
val minX = math.max(0, math.floor(math.min(a.x, math.min(b.x,c.x))).toInt)
val maxX = math.min(w - 1, math.ceil(math.max(a.x, math.max(b.x,c.x))).toInt)
val minY = math.max(0, math.floor(math.min(a.y, math.min(b.y,c.y))).toInt)
val maxY = math.min(h - 1, math.ceil(math.max(a.y, math.max(b.y,c.y))).toInt)
if (minX > maxX || minY > maxY) return
val rgb = 0xff000000 | (color.getRed << 16) | (color.getGreen << 8) | color.getBlue
var y = minY
while (y <= maxY) {
val py = y + 0.5
var x = minX
while (x <= maxX) {
val px = x + 0.5
val wa = ((b.y-c.y)*(px-c.x) + (c.x-b.x)*(py-c.y)) / area
val wb = ((c.y-a.y)*(px-c.x) + (a.x-c.x)*(py-c.y)) / area
val wc = 1.0 - wa - wb
if (wa >= -0.00001 && wb >= -0.00001 && wc >= -0.00001) {
val inv = wa*a.invZ + wb*b.invZ + wc*c.invZ
val idx = y*w + x
if (inv > zbuf(idx)) {
zbuf(idx) = inv
imgPixels(idx) = rgb
}
}
x += 1
}
y += 1
}
}
def renderImage(w0: Int, h0: Int): BufferedImage = {
val w = math.max(300,w0); val h = math.max(300,h0)
val img = new BufferedImage(w,h,BufferedImage.TYPE_INT_ARGB)
val bg = img.createGraphics()
bg.setPaint(new GradientPaint(0,0,backgroundTop,0,h,backgroundBottom))
bg.fillRect(0,0,w,h)
// A subtle floor horizon glow.
bg.setColor(new Color(174,113,43,22))
bg.fillOval(w/2 - 340,h/2 + 100,680,300)
bg.dispose()
val pixels = img.getRaster.getDataBuffer.asInstanceOf[DataBufferInt].getData
val zbuf = Array.fill(w*h)(0.0)
triangles.foreach { t =>
val av = transform(t.a); val bv = transform(t.b); val cv = transform(t.c)
val clipped = clipNear(scala.collection.immutable.List(av,bv,cv))
if (clipped.length >= 3) {
val first = project(clipped.head,w,h)
var i = 1
while (i < clipped.length - 1) {
rasterTriangle(pixels,zbuf,w,h,first,
project(clipped(i),w,h),project(clipped(i+1),w,h),t.color)
i += 1
}
}
}
val g = img.createGraphics()
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON)
drawOverlay(g,w,h)
g.dispose()
img
}
def projectWorld(x: Double,y: Double,z: Double,w: Int,h: Int): Point = {
val p = transform(V(x,y,z))
if (p.z <= nearPlane) new Point(w/2,h/2)
else {
val s = project(p,w,h)
new Point(s.x.toInt,s.y.toInt)
}
}
def drawOverlay(g: Graphics2D,w: Int,h: Int): Unit = {
// Header and soft dark band.
g.setColor(new Color(4,8,22,168))
g.fillRoundRect(16,14,w-32,66,22,22)
g.setFont(new Font("Serif",Font.BOLD,26))
g.setColor(new Color(255,218,140))
val title = "DR. BABASAHEB AMBEDKAR"
g.drawString(title,(w-g.getFontMetrics.stringWidth(title))/2,43)
g.setFont(new Font("SansSerif",Font.PLAIN,13))
g.setColor(new Color(235,238,250))
val subtitle = "3D STATUE STUDIO | DRAG TO ROTATE | SCROLL TO ZOOM"
g.drawString(subtitle,(w-g.getFontMetrics.stringWidth(subtitle))/2,64)
// Label over the front face of the pedestal when viewed near-front-on.
if (math.abs(yaw) < 0.55 || math.abs(yaw - 2*math.Pi) < 0.55) {
val p = projectWorld(0,0.42,0.81,w,h)
g.setFont(new Font("Serif",Font.BOLD,10))
g.setColor(new Color(255,221,142))
val txt = "DR. B. R. AMBEDKAR"
g.drawString(txt,p.x-g.getFontMetrics.stringWidth(txt)/2,p.y+4)
}
// Bottom help strip.
g.setColor(new Color(3,6,17,185))
g.fillRoundRect(18,h-47,w-36,30,14,14)
g.setFont(new Font("SansSerif",Font.PLAIN,12))
g.setColor(new Color(237,240,250))
val help = "Mouse drag: rotate | Wheel / +/-: zoom | A/D: rotate | Arrows: tilt | 1-4: themes | H: halo | B: book | R: reset"
g.drawString(help,30,h-28)
}
// --------------------------------------------------------------
// UI, themes and screenshot export.
// --------------------------------------------------------------
var canvas: JPanel = _
var statusLabel: JLabel = _
var frame: JFrame = _
def resetCamera(): Unit = {
yaw = 0.0
pitch = 0.02
zoom = 9.4
if (canvas != null) canvas.repaint()
}
def saveScreenshot(): Unit = {
val chooser = new JFileChooser()
chooser.setDialogTitle("Save statue screenshot as PNG")
chooser.setSelectedFile(new File("Babasaheb_3D_Statue.png"))
if (chooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
try {
val image = renderImage(canvas.getWidth,canvas.getHeight)
val target = chooser.getSelectedFile
val file = if (target.getName.toLowerCase.endsWith(".png")) target
else new File(target.getParentFile,target.getName + ".png")
ImageIO.write(image,"png",file)
statusLabel.setText("Saved: " + file.getAbsolutePath)
} catch {
case ex: Exception =>
JOptionPane.showMessageDialog(frame,"Could not save screenshot: " + ex.getMessage,
"Save Error",JOptionPane.ERROR_MESSAGE)
}
}
}
def makeButton(text: String): JButton = {
val b = new JButton(text)
b.setFont(new Font("SansSerif",Font.BOLD,13))
b.setFocusPainted(false)
b.setBackground(new Color(28,43,81))
b.setForeground(new Color(245,240,225))
b.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(207,164,75),1),
BorderFactory.createEmptyBorder(7,10,7,10)))
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
b
}
def start(): Unit = {
buildScene()
SwingUtilities.invokeLater(new Runnable {
override def run(): Unit = {
frame = new JFrame("Babasaheb 3D Statue Studio - Scala")
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.setMinimumSize(new Dimension(800,650))
val root = new JPanel(new BorderLayout(0,0))
root.setBackground(new Color(8,12,28))
val tools = new JPanel(new FlowLayout(FlowLayout.CENTER,8,8))
tools.setBackground(new Color(13,20,43))
val themeBox = new JComboBox[String](Array(
"Classic Blue Suit", "Bronze Sculpture", "Golden Tribute", "White Marble"
))
themeBox.setFont(new Font("SansSerif",Font.BOLD,13))
themeBox.setPreferredSize(new Dimension(175,34))
themeBox.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
themeMode = themeBox.getSelectedIndex
buildScene()
canvas.repaint()
}
})
val front = makeButton("FRONT VIEW")
front.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = resetCamera()
})
val halo = makeButton("TOGGLE HALO (H)")
halo.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
showHalo = !showHalo; buildScene(); canvas.repaint()
statusLabel.setText("Halo: " + (if (showHalo) "ON" else "OFF"))
}
})
val book = makeButton("TOGGLE BOOK (B)")
book.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = {
showBook = !showBook; buildScene(); canvas.repaint()
statusLabel.setText("Constitution book: " + (if (showBook) "ON" else "OFF"))
}
})
val shot = makeButton("SAVE PNG")
shot.addActionListener(new ActionListener {
override def actionPerformed(e: ActionEvent): Unit = saveScreenshot()
})
tools.add(new JLabel("STATUE STYLE:"))
tools.getComponent(0).asInstanceOf[JLabel].setForeground(new Color(242,222,173))
tools.add(themeBox); tools.add(front); tools.add(halo); tools.add(book); tools.add(shot)
root.add(tools,BorderLayout.NORTH)
canvas = new JPanel() {
setOpaque(true)
setBackground(Color.BLACK)
setFocusable(true)
override def paintComponent(g: Graphics): Unit = {
super.paintComponent(g)
val image = renderImage(getWidth,getHeight)
g.drawImage(image,0,0,null)
}
}
root.add(canvas,BorderLayout.CENTER)
statusLabel = new JLabel(" Ready ? drag the model to inspect it from every side.")
statusLabel.setFont(new Font("SansSerif",Font.PLAIN,12))
statusLabel.setForeground(new Color(230,230,245))
statusLabel.setBorder(BorderFactory.createEmptyBorder(7,12,7,12))
statusLabel.setOpaque(true)
statusLabel.setBackground(new Color(10,15,31))
root.add(statusLabel,BorderLayout.SOUTH)
var lastX = 0
var lastY = 0
canvas.addMouseListener(new MouseAdapter {
override def mousePressed(e: MouseEvent): Unit = {
lastX = e.getX; lastY = e.getY
canvas.requestFocusInWindow()
}
})
canvas.addMouseMotionListener(new MouseMotionAdapter {
override def mouseDragged(e: MouseEvent): Unit = {
val dx = e.getX - lastX
val dy = e.getY - lastY
yaw += dx * 0.009
pitch = clamp(pitch + dy * 0.006,-0.72,0.72)
lastX = e.getX; lastY = e.getY
canvas.repaint()
}
})
canvas.addMouseWheelListener(new MouseWheelListener {
override def mouseWheelMoved(e: MouseWheelEvent): Unit = {
zoom = clamp(zoom + e.getWheelRotation * 0.45,6.3,15.5)
canvas.repaint()
}
})
canvas.addKeyListener(new KeyAdapter {
override def keyPressed(e: KeyEvent): Unit = {
e.getKeyCode match {
case KeyEvent.VK_A | KeyEvent.VK_LEFT => yaw -= 0.12
case KeyEvent.VK_D | KeyEvent.VK_RIGHT => yaw += 0.12
case KeyEvent.VK_UP => pitch = clamp(pitch - 0.06,-0.72,0.72)
case KeyEvent.VK_DOWN => pitch = clamp(pitch + 0.06,-0.72,0.72)
case KeyEvent.VK_ADD | KeyEvent.VK_EQUALS => zoom = clamp(zoom - 0.4,6.3,15.5)
case KeyEvent.VK_SUBTRACT | KeyEvent.VK_MINUS => zoom = clamp(zoom + 0.4,6.3,15.5)
case KeyEvent.VK_H => showHalo = !showHalo; buildScene()
case KeyEvent.VK_B => showBook = !showBook; buildScene()
case KeyEvent.VK_R => resetCamera()
case KeyEvent.VK_1 => themeMode = 0; themeBox.setSelectedIndex(0); buildScene()
case KeyEvent.VK_2 => themeMode = 1; themeBox.setSelectedIndex(1); buildScene()
case KeyEvent.VK_3 => themeMode = 2; themeBox.setSelectedIndex(2); buildScene()
case KeyEvent.VK_4 => themeMode = 3; themeBox.setSelectedIndex(3); buildScene()
case _ => ()
}
canvas.repaint()
}
})
frame.setContentPane(root)
frame.setSize(1120,850)
frame.setLocationRelativeTo(null)
frame.setVisible(true)
canvas.requestFocusInWindow()
}
})
}
}
Babasaheb3DStatue.start()