# %------------------------------------------- Packages --------------------------------------------% #
importnumpyasnpimportmatplotlib.pyplotaspltfrommpl_toolkits.mplot3d.art3dimportPoly3DCollectionfrommatplotlib.cmimportScalarMappablefrommatplotlib.colorsimportNormalize# %--------------------------------------- Helper Functions ----------------------------------------% #
defcylinder(R,Ntheta=20):# Define angles around the circumference
theta=np.linspace(0,2*np.pi,Ntheta+1)# Define the height levels (2 levels: bottom and top)
z=np.array([0,1])# Compute X, Y, Z coordinates
X=np.outer([R,R],np.cos(theta))Y=np.outer([R,R],np.sin(theta))Z=np.outer(z,np.ones_like(theta))returnX,Y,Zdefmake_alphabet(resolution=1000):# Grid for the alphabet
x1=np.linspace(0,1,resolution)x2=x1[::-1]# Helper function
hyp=lambdax,deg:(1-x**deg)**(1/deg)# Make the alphabet
alphabet={'A':np.array([[0,0.5,1,0.75,0.25],[0,1,0,0.5,0.5]]),'B':np.array([[0,0,*x1,*x2,*x1,*x2],[0,1,*(hyp(x1,3)/4+0.75),*(-hyp(x2,3)/4+0.75),*(hyp(x1,4)/4+0.25),*(-hyp(x2,4)/4+0.25)]]),'C':np.array([[*(x2/2+0.5),*(-x1/2+0.5),*(-x2/2+0.5),*(x1/2+0.5)],[*(hyp(x2,3)/3+2/3),*(hyp(x1,2)/2+0.5),*(-hyp(x2,2)/2+0.5),*(-hyp(x1,3)/3+1/3)]]),'D':np.array([[0,0,*x1,*x2],[0,1,*(hyp(x1,3)/2+0.5),*(-hyp(x2,3)/2+0.5)]]),'E':np.array([[1,0,0,1,0,0,1],[1,1,0.5,0.5,0.5,0,0]]),'F':np.array([[1,0,0,0,1],[1,1,0,0.5,0.5]]),'G':np.array([[*(x2/2+0.5),*(-x1/2+0.5),*(-x2/2+0.5),*(x1/2+0.5),0.6,1,1],[*(hyp(x2,3)/4+3/4),*(hyp(x1,2)/2+0.5),*(-hyp(x2,2)/2+0.5),*(-hyp(x1,3)/5*2+2/5),2/5,2/5,0]]),'H':np.array([[0,0,0,1,1,1],[1,0,0.5,0.5,1,0]]),'I':np.array([[0.25,0.75,0.5,0.5,0.75,0.25],[1,1,1,0,0,0]]),'J':np.array([[0.25,0.75,0.5,0.5,*(x2/4+0.25),*(-x1/4+0.25)],[1,1,1,0.25,*(-hyp(x2,3)/4+0.25),*(-hyp(x1,2)/4+0.25)]]),'K':np.array([[0,0,0,1,0,1],[1,0,0.5,1,0.5,0]]),'L':np.array([[0,0,1],[1,0,0]]),'M':np.array([[0,0,0.5,1,1],[0,1,0,1,0]]),'N':np.array([[0,0,1,1],[0,1,0,1]]),'O':np.array([[*(-x2/2+0.5),*(x1/2+0.5),*(x2/2+0.5),*(-x1/2+0.5)],[*(hyp(x2,3)/2+0.5),*(hyp(x1,3)/2+0.5),*(-hyp(x2,3)/2+0.5),*(-hyp(x1,3)/2+0.5)]]),'P':np.array([[0,0,*x1,*x2],[0,1,*(hyp(x1,3)/4+0.75),*(-hyp(x2,4)/4+0.75)]]),'Q':np.array([[*(-x2/2+0.5),*(x1/2+0.5),*(x2/2+0.5),*(-x1/2+0.5),0.5,0.9],[*(hyp(x2,3)/2+0.5),*(hyp(x1,3)/2+0.5),*(-hyp(x2,3)/2+0.5),*(-hyp(x1,3)/2+0.5),1/3,0.0]]),'R':np.array([[0,0,*x1,*x2,1],[0,1,*(hyp(x1,3)/4+0.75),*(-hyp(x2,4)/4+0.75),0]]),'S':np.array([[*(x2/2+0.5),*(-x1/2+0.5),*(-x2/2+0.5),*(x1/2+0.5),*(x2/2+0.5),*(-x1/2+0.5)],[*(hyp(x2,3)/3+2/3),*(hyp(x1,4)/4+3/4),*(-hyp(x2,4)/4+3/4),*(hyp(x1,4)/4+0.25),*(-hyp(x2,3)/3+1/3),*(-hyp(x1,4)/4+1/4)]]),'T':np.array([[0,1,0.5,0.5],[1,1,1,0]]),'U':np.array([[0,*(-x2/2+0.5),*(x1/2+0.5),1],[1,*(-hyp(x2,3)/3+1/3),*(-hyp(x1,3)/3+1/3),1]]),'V':np.array([[0,0.5,1],[1,0,1]]),'W':np.array([[0,0.25,0.5,0.75,1],[1,0,1,0,1]]),'X':np.array([[1,0,0.5,0,1],[1,0,0.5,1,0]]),'Y':np.array([[0,0.5,1,0.5,0.5],[1,2/3,1,2/3,0]]),'Z':np.array([[0,1,0,1],[1,1,0,0]]),}returnalphabet# %------------------------------------------ Cake Class -------------------------------------------% #
classCake():def__init__(self,R=5,H=2,dr=4.5,r=0.15,h=0.5,cake_res=1000,Ntheta=100)->None:# Cake parameters
self.R=R# Cake radius
self.H=H# Cake height
self.dr=dr# Decor radius
self.r=r# Candle radius
self.h=h# Candle height
self.cake_res=cake_res# Plotting parameters
self.tg_flg=Falseself.Ntheta=Nthetaself.theta=np.linspace(0,2*np.pi,self.Ntheta)self.cake_color={'cake':[0.4,0.25,0.1],'frosting':[1.0,0.9,0.95],'decor1':[0.85,0.35,0.15],'decor2':[0.85,0.25,0.50],'candle':[1.0,0.08,0.01],'text':[0.35,0.2,0.1],}# Create figure
self.cake_fig,self.cake_ax=self.make_fig()# Make the cake
self.make_cake()# Make the candles
self.make_candles()# Add top message drawn on the frosting surface
self.add_top_message_curves("Happy Birthday",rotation_deg=-self.cake_ax.azim)defmake_fig(self):# Create figure
cake_fig=plt.figure(figsize=(10,8))cake_ax=cake_fig.add_subplot(111,projection='3d',computed_zorder=False)# Remove axes
cake_ax.set_axis_off()# Remove background
cake_ax.xaxis.set_pane_color((1.0,1.0,1.0,0.0))# X-axis background to transparent
cake_ax.yaxis.set_pane_color((1.0,1.0,1.0,0.0))# Y-axis background to transparent
cake_ax.zaxis.set_pane_color((1.0,1.0,1.0,0.0))# Z-axis background to transparent
# Remove grid lines
cake_ax.grid(False)# remove ticks
cake_ax.set_xticks([])cake_ax.set_yticks([])cake_ax.set_zticks([])cake_ax.set_box_aspect([1,1,1])# return figure and axis
returncake_fig,cake_axdefmake_cake(self):# Base layer (bottom of the cake)
x_c=self.R*np.cos(self.theta)y_c=self.R*np.sin(self.theta)z_c=np.zeros_like(self.theta)# Cake Cylinder
x_cake,y_cake,z_cake=cylinder(self.R,self.cake_res)# Vertical sides of the bottom part
self.cake_ax.plot_surface(x_cake,y_cake,z_cake*1*self.H/3-self.H,color=self.cake_color['cake'],linewidth=0,zorder=1.1)# Vertical sides of the top part
self.cake_ax.plot_surface(x_cake,y_cake,z_cake*2*self.H/3-2*self.H/3,color=self.cake_color['frosting'],linewidth=0,zorder=2)# Top layer
self.cake_ax.add_collection3d(Poly3DCollection([list(zip(x_c,y_c,z_c+0.01))],color=self.cake_color['frosting'],linewidths=0,zorder=2))# Bottom layer
self.cake_ax.add_collection3d(Poly3DCollection([list(zip(x_c,y_c,z_c-self.H-0.01))],color=self.cake_color['cake'],linewidths=0,zorder=1))self.cake_ax.plot(x_c,y_c,z_c,color=self.cake_color['decor1'],linestyle='-',linewidth=1,marker='o',markerfacecolor=self.cake_color['decor2'],markersize=8,zorder=3)# Inner circle decoration
self.cake_ax.plot(self.dr*np.cos(self.theta),self.dr*np.sin(self.theta),z_c,color=self.cake_color['decor1'],linestyle='-',linewidth=3,zorder=3.1)defmake_candles(self):# Candle positions
theta_candles_pos=np.linspace(0,2*np.pi,10,endpoint=False)x_candles=self.dr*np.cos(theta_candles_pos)y_candles=self.dr*np.sin(theta_candles_pos)z_candles=np.linspace(0,self.h,50)theta_candles,z_candles=np.meshgrid(self.theta,z_candles)# Flame parameters
flame_radius=0.1# Base radius of the flame
flame_height=0.3# Height of the flame
flame_base_height=self.hflame_middle_height=self.h+flame_height/2flame_tip_height=self.h+flame_heightflame_theta1=np.linspace(0,2*np.pi,50)flame_theta2=np.linspace(0,2*np.pi,50)flame_z1=np.linspace(flame_base_height,flame_middle_height,50)flame_z2=np.linspace(flame_middle_height,flame_tip_height,50)flame_theta2,flame_z2=np.meshgrid(flame_theta2,flame_z2)flame_theta1,flame_z1=np.meshgrid(flame_theta1,flame_z1)# Define colormap for flame color gradient
cmap=plt.cm.hot# Red-to-yellow colormap
norm=Normalize(vmin=flame_base_height,vmax=flame_tip_height)# Plot candles
forx,yinzip(x_candles,y_candles):# Candle surface
X=x+self.r*np.cos(theta_candles)# x-coordinates of candle surface
Y=y+self.r*np.sin(theta_candles)# y-coordinates of candle surface
Z=z_candles# z-coordinates of candle surface
self.cake_ax.plot_surface(X,Y,Z,color=self.cake_color['candle'],edgecolor='none',alpha=1,zorder=5)# Parametric equation for the first cone
flame_x1=x+(flame_radius*(flame_z1-flame_base_height)/0.25)*np.cos(flame_theta1)flame_y1=y+(flame_radius*(flame_z1-flame_base_height)/0.25)*np.sin(flame_theta1)colors1=cmap(norm(flame_z1))# Color gradient for the first cone
# Parametric equation for the second cone
flame_x2=x+(flame_radius*(1-(flame_z2-flame_middle_height)/0.25))*np.cos(flame_theta2)flame_y2=y+(flame_radius*(1-(flame_z2-flame_middle_height)/0.25))*np.sin(flame_theta2)colors2=cmap(norm(flame_z2))# Color gradient for the second cone
# Plot the flames with gradient
self.cake_ax.plot_surface(flame_x1,flame_y1,flame_z1,facecolors=colors1,edgecolor='none',alpha=1,zorder=6)self.cake_ax.plot_surface(flame_x2,flame_y2,flame_z2,facecolors=colors2,edgecolor='none',alpha=1,zorder=6)defadd_top_message_curves(self,message,scale=0.28,spacing=0.3,z_offset=0.02,rotation_deg=0.0):# Draw each letter as a set of 3D curves on the top frosting plane
alphabet=make_alphabet()advance=scale+spacingstart_x=-(len(message)*advance)/2# center the word
cursor=start_xtheta=np.deg2rad(rotation_deg)rot=np.array([[np.cos(theta),-np.sin(theta)],[np.sin(theta),np.cos(theta)]])forcharinmessage.upper():ifchar=='':cursor+=advancecontinueifcharnotinalphabet:cursor+=advancecontinueletter=alphabet[char]x=letter[0]*scale+cursory=letter[1]*scalez=np.zeros_like(x)+z_offset# sit just above the frosting
xy=np.vstack((x,y))x_rot,y_rot=rot@xy# Light outline for visibility against frosting
self.cake_ax.plot(x_rot,y_rot,z,color='white',linewidth=1,alpha=0.8,zorder=4)self.cake_ax.plot(x_rot,y_rot,z,color='black',linewidth=1.5,zorder=4.1)cursor+=advance# %--------------------------------------------- Main ----------------------------------------------% #
defmain():# Plot a cake
cake=Cake(cake_res=300,Ntheta=50)# %--------------------------------------------- Run -----------------------------------------------% #
if__name__=='__main__':print(f'{"Start":-^{50}}')main()plt.show()print(f'{"End":-^{50}}')