Загрузка данных
import tkinter as tk
import math
# задаётся куб
VERTICES = [
(-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1),
(-1,-1, 1),(1,-1, 1),(1,1, 1),(-1,1, 1),
]
FACES = [
([4,5,6,7], 1, ( 0, 0, 1)),
([1,0,3,2], 2, ( 0, 0,-1)),
([0,1,5,4], 3, ( 0,-1, 0)),
([3,7,6,2], 4, ( 0, 1, 0)),
([0,4,7,3], 5, (-1, 0, 0)),
([5,1,2,6], 6, ( 1, 0, 0)),
]
FACE_BG = {
1:"#F0F0F0", 2:"#1a1a2e",
3:"#e63946", 4:"#1565C0",
5:"#2e7d32", 6:"#E65100",
}
FACE_FG = {
1:"#1a1a2e", 2:"#F0F0F0",
3:"#F0F0F0", 4:"#F0F0F0",
5:"#F0F0F0", 6:"#1a1a2e",
}
# поворот
def rot_x(v,a):
x,y,z=v; c,s=math.cos(a),math.sin(a)
return (x, y*c-z*s, y*s+z*c)
def rot_y(v,a):
x,y,z=v; c,s=math.cos(a),math.sin(a)
return (x*c+z*s, y, -x*s+z*c)
def rot_z(v,a):
x,y,z=v; c,s=math.cos(a),math.sin(a)
return (x*c-y*s, x*s+y*c, z)
def dot3(a,b): return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]
def normalize3(v):
L=math.sqrt(dot3(v,v))
return (v[0]/L,v[1]/L,v[2]/L) if L>1e-9 else v
def cross2d(ax,ay,bx,by): return ax*by - ay*bx
# проекция 3д в 2д
def project(v, scale, ox, oy, fov=5.0):
x,y,z=v
d=fov+z
if d<0.01: d=0.01
return (ox + x*scale*fov/d,
oy - y*scale*fov/d)
def shade_color(hex6, bright):
r=int(hex6[1:3],16); g=int(hex6[3:5],16); b=int(hex6[5:7],16)
r=min(255,int(r*bright)); g=min(255,int(g*bright)); b=min(255,int(b*bright))
return f"#{r:02x}{g:02x}{b:02x}"
def face_area_2d(pts2):
n=len(pts2); s=0.0
for i in range(n):
x0,y0=pts2[i]; x1,y1=pts2[(i+1)%n]
s+=(x0*y1 - x1*y0)
return s/2.0
LIGHT = normalize3((0.5, 0.8, 1.0))
class DiceRenderer:
def __init__(self, canvas):
self.canvas = canvas
def render(self, ax, ay, az, scale):
c=self.canvas
c.delete("all")
W=c.winfo_width() or 560
H=c.winfo_height() or 560
ox,oy=W/2, H/2
c.create_rectangle(0,0,W,H, fill="#1a1a2e", outline="")
verts=[]
for v in VERTICES:
v=rot_x(v,ax); v=rot_y(v,ay); v=rot_z(v,az)
verts.append(v)
face_data=[]
for vi, fnum, n0 in FACES:
pts3=[verts[j] for j in vi]
zc=sum(p[2] for p in pts3)/4
n=rot_x(n0,ax); n=rot_y(n,ay); n=rot_z(n,az)
face_data.append((zc, fnum, pts3, n))
face_data.sort(key=lambda x: x[0])
for zc, fnum, pts3, normal in face_data:
pts2=[project(p,scale,ox,oy) for p in pts3]
area=face_area_2d(pts2)
if area < 0:
continue
if abs(area) < 1.0:
continue
diff=max(0.0, dot3(normal, LIGHT))
bright=0.38 + 0.62*diff
bg=shade_color(FACE_BG[fnum], bright)
spec=diff**6
ec=shade_color(FACE_BG[fnum], min(1.0, bright+spec*0.35))
flat=[coord for p in pts2 for coord in p]
c.create_polygon(flat, fill=bg, outline=ec,
width=max(1,int(scale*0.035)))
cx=sum(p[0] for p in pts2)/4
cy=sum(p[1] for p in pts2)/4
# размер символа от размера площади проекции
sym_size=max(6, int(math.sqrt(abs(area))*0.38))
self._label(c, fnum, cx, cy, pts2, sym_size, area, FACE_FG[fnum])
def _label(self, c, fnum, cx, cy, pts2, sym, area, fg):
inset=math.sqrt(abs(area))*0.18
if fnum == 1:
c.create_text(cx, cy, text="1", fill=fg,
font=("Georgia", sym, "bold"))
elif fnum == 2:
c.create_text(cx, cy, text="2", fill=fg,
font=("Georgia", sym, "bold"))
elif fnum == 3:
r=max(2, int(sym*0.22))
sp=inset*0.72
for dx,dy in [(-sp,-sp),(sp,-sp),(0,0),(-sp,sp),(sp,sp)]:
x0,y0=cx+dx-r, cy+dy-r
c.create_oval(x0,y0,x0+2*r,y0+2*r, fill=fg, outline="")
elif fnum == 4:
side=inset*1.5
h3=side*math.sqrt(3)/2
pts_t=[cx, cy-h3*0.62,
cx+side/2, cy+h3*0.42,
cx-side/2, cy+h3*0.42]
c.create_polygon(pts_t, fill=fg, outline="")
elif fnum == 5:
pts_s=[]
for i in range(10):
a=-math.pi/2 + i*math.pi/5
r2=(sym*0.52) if i%2==0 else (sym*0.22)
pts_s.extend([cx+math.cos(a)*r2, cy+math.sin(a)*r2])
c.create_polygon(pts_s, fill=fg, outline="")
elif fnum == 6:
r=max(2, int(sym*0.20))
sp=inset*0.68
cols=["#e63946","#f4a261","#2a9d8f",
"#457b9d","#a8dadc","#f8edeb"]
pos6=[(-sp,-sp),(sp,-sp),
(-sp, 0),(sp, 0),
(-sp, sp),(sp, sp)]
for (dx,dy),col in zip(pos6,cols):
x0,y0=cx+dx-r, cy+dy-r
c.create_oval(x0,y0,x0+2*r,y0+2*r, fill=col, outline="")
class DiceApp:
def __init__(self, root):
self.root=root
self.root.title("3D Dice Viewer")
self.root.configure(bg="#0f0f1a")
self.root.resizable(True,True)
self.ax=tk.DoubleVar(value=0.4)
self.ay=tk.DoubleVar(value=0.6)
self.az=tk.DoubleVar(value=0.0)
self.scale=tk.DoubleVar(value=160.0)
self._drag=None
self._anim_id=None
self._auto_spin=False
self._fullscreen=False
self._build_ui()
self._bind()
self.draw()
try:
self.root.state("zoomed")
except Exception:
try: self.root.attributes("-zoomed",True)
except Exception: self.root.geometry("920x680")
def _build_ui(self):
tb=tk.Frame(self.root, bg="#12122a", height=38)
tb.pack(side="top", fill="x")
tb.pack_propagate(False)
def tbtn(text, cmd, accent=False):
b=tk.Button(tb, text=text, command=cmd, relief="flat",
bg="#2563eb" if accent else "#1e1e3a",
fg="white", font=("Arial",9), cursor="hand2",
padx=10, pady=6,
activebackground="#3b3b5c", activeforeground="white")
b.pack(side="left", padx=2, pady=4)
return b
tbtn("⛶ Полный экран", self.toggle_fullscreen)
tk.Frame(tb,bg="#333355",width=1).pack(side="left",fill="y",pady=6,padx=4)
tbtn("↺ Сбросить вид", self.reset_view)
self._spin_btn=tbtn("▶ Авто-вращение", self.toggle_spin, accent=True)
main=tk.Frame(self.root, bg="#0f0f1a")
main.pack(fill="both", expand=True)
self.canvas=tk.Canvas(main, bg="#1a1a2e",
highlightthickness=0, cursor="hand2")
self.canvas.pack(side="left", fill="both", expand=True,
padx=(8,4), pady=(4,8))
self.renderer=DiceRenderer(self.canvas)
rp=tk.Frame(main, bg="#12122a", width=230)
rp.pack(side="right", fill="y", padx=(0,8), pady=(4,8))
rp.pack_propagate(False)
tk.Label(rp, text="ТОЧКА ЗРЕНИЯ", bg="#12122a", fg="#8888aa",
font=("Arial",8,"bold")).pack(pady=(14,6))
self._sl(rp,"Ось X",self.ax,-math.pi,math.pi)
self._sl(rp,"Ось Y",self.ay,-math.pi,math.pi)
self._sl(rp,"Ось Z", self.az,-math.pi,math.pi)
tk.Frame(rp,bg="#222244",height=1).pack(fill="x",padx=10,pady=8)
tk.Label(rp,text="МАСШТАБ",bg="#12122a",fg="#8888aa",
font=("Arial",8,"bold")).pack(pady=(0,4))
self._sl(rp,"Размер",self.scale,60,400,integer=True)
tk.Frame(rp,bg="#222244",height=1).pack(fill="x",padx=10,pady=8)
tk.Label(rp,text="ГРАНИ",bg="#12122a",fg="#8888aa",
font=("Arial",8,"bold")).pack(pady=(0,4))
for fnum,(bg,lbl) in {
1:("#F0F0F0","1 — цифра (белая)"),
2:("#1a1a2e","2 — цифра (чёрная)"),
3:("#e63946","3 — пять точек"),
4:("#1565C0","4 — треугольник"),
5:("#2e7d32","5 — звезда"),
6:("#E65100","6 — цвет. маркеры"),
}.items():
row=tk.Frame(rp,bg="#12122a")
row.pack(fill="x",padx=12,pady=1)
dot=tk.Canvas(row,width=16,height=16,bg="#12122a",highlightthickness=0)
dot.pack(side="left")
dot.create_rectangle(1,1,15,15,fill=bg,outline="#555577")
tk.Label(row,text=lbl,bg="#12122a",fg="#ccccee",
font=("Arial",9)).pack(side="left",padx=6)
def _sl(self, parent, label, var, lo, hi, integer=False):
frm=tk.Frame(parent,bg="#12122a")
frm.pack(fill="x",padx=10,pady=3)
tk.Label(frm,text=label,bg="#12122a",fg="#aaaacc",
font=("Arial",9),width=16,anchor="w").pack(side="left")
res=200 if not integer else int(hi-lo)
tk.Scale(frm,variable=var,from_=lo,to=hi,
orient="horizontal",bg="#12122a",fg="#aaaacc",
troughcolor="#222244",highlightthickness=0,
showvalue=False,length=110,resolution=(hi-lo)/res,
command=lambda _: self.draw()).pack(side="left")
def _bind(self):
c=self.canvas
c.bind("<ButtonPress-1>", self._ds)
c.bind("<B1-Motion>", self._dm)
c.bind("<ButtonRelease-1>",self._de)
c.bind("<MouseWheel>", self._wheel)
c.bind("<Button-4>", self._wheel)
c.bind("<Button-5>", self._wheel)
self.root.bind("<F11>", lambda e: self.toggle_fullscreen())
self.root.bind("<Escape>", lambda e: self._xfs())
self.root.bind("<Left>", lambda e: self._nudge("y",-0.06))
self.root.bind("<Right>", lambda e: self._nudge("y", 0.06))
self.root.bind("<Up>", lambda e: self._nudge("x",-0.06))
self.root.bind("<Down>", lambda e: self._nudge("x", 0.06))
self.root.bind("q", lambda e: self._nudge("z",-0.06))
self.root.bind("e", lambda e: self._nudge("z", 0.06))
self.root.bind("+", lambda e: self._ns(12))
self.root.bind("=", lambda e: self._ns(12))
self.root.bind("-", lambda e: self._ns(-12))
self.root.bind("<Configure>", lambda e: self.draw())
def _nudge(self,axis,d):
v={"x":self.ax,"y":self.ay,"z":self.az}[axis]
v.set(v.get()+d)
self.draw()
def _ns(self,d):
self.scale.set(max(60,min(400,self.scale.get()+d)))
self.draw()
def _ds(self,e): self._drag=(e.x,e.y,self.ax.get(),self.ay.get())
def _dm(self,e):
if not self._drag: return
sx,sy,ax0,ay0=self._drag
self.ax.set(ax0+(e.y-sy)*0.008)
self.ay.set(ay0+(e.x-sx)*0.008)
self.draw()
def _de(self,e): self._drag=None
def _wheel(self,e):
d=10*(1 if e.num==4 else -1 if e.num==5 else e.delta/120)
self.scale.set(max(60,min(400,self.scale.get()+d)))
self.draw()
def toggle_fullscreen(self):
self._fullscreen=not self._fullscreen
self.root.attributes("-fullscreen",self._fullscreen)
def _xfs(self):
if self._fullscreen:
self._fullscreen=False
self.root.attributes("-fullscreen",False)
def toggle_spin(self):
self._auto_spin=not self._auto_spin
if self._auto_spin:
self._spin_btn.configure(text="⏹ Стоп",bg="#e63946")
self._anim()
else:
self._spin_btn.configure(text="▶ Авто-вращение",bg="#2563eb")
if self._anim_id:
self.root.after_cancel(self._anim_id)
self._anim_id=None
def _anim(self):
if not self._auto_spin: return
self.ay.set(self.ay.get()+0.018)
self.ax.set(self.ax.get()+0.005)
self.draw()
self._anim_id=self.root.after(16,self._anim)
def reset_view(self):
self.ax.set(0.4); self.ay.set(0.6); self.az.set(0.0)
self.scale.set(160); self.draw()
def draw(self,_=None):
self.renderer.render(self.ax.get(),self.ay.get(),
self.az.get(),self.scale.get())
if __name__=="__main__":
root=tk.Tk()
DiceApp(root)
root.mainloop()