找回密码
 立即注册
搜索
查看: 257|回复: 8

【台风预报制图python】误差圈和4象限风圈的画法,以FNV3为例

[复制链接]

1

主题

138

回帖

305

积分

热带低压

积分
305
发表于 2025-8-13 20:32 | 显示全部楼层 |阅读模式
本帖最后由 菜园子 于 2025-8-13 20:50 编辑

我现在已经懒到连日期拆分都用大模型写了哈哈哈,使用大模型写代码真是方便,只要你有想象力,就没有做不出来的功能
以下代码应该可以直接运行,前提是要装好cartopy(运行时需要先把FNV3的相关文件放在指定文件夹中,下载地址为https://deepmind.google.com/science/weatherlab,需要梯子,你也可以自己修改指定文件夹)

代码中不懂的可以问大模型,现在的大模型真是很好的老师,我也是做大模型方向的,一起感受大模型的魅力吧~

  1. import pandas as pd
  2. import numpy as np
  3. import matplotlib.pyplot as plt
  4. import matplotlib.patches as patches
  5. import cartopy.crs as ccrs
  6. import cartopy.feature as cfeature
  7. from cartopy.io.shapereader import Reader
  8. from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
  9. import matplotlib.ticker as mticker
  10. from scipy.interpolate import PchipInterpolator
  11. from shapely.geometry import Polygon, MultiPolygon
  12. from shapely.ops import unary_union
  13. from datetime import datetime, timedelta
  14. import os
  15. import shutil
  16. import re



  17. def extract_init_time(filename):
  18.     """
  19.     从文件名中提取init_time(格式:YYYYMMDDHHZ)
  20.     示例文件名:FNV3_2025_08_13T06_00_paired.csv → 返回 2025081306Z
  21.     """
  22.     try:
  23.         # 使用正则表达式匹配日期时间部分
  24.         match = re.search(r'(\d{4})_(\d{2})_(\d{2})T(\d{2})_\d{2}', filename)
  25.         if match:
  26.             year, month, day, hour = match.groups()
  27.             return f"{year}{month}{day}{hour}Z"
  28.         else:
  29.             raise ValueError("文件名格式不符合要求")
  30.     except Exception as e:
  31.         print(f"Error extracting init_time: {e}")
  32.         raise

  33. def parse_lead_time(lead_time_str):
  34.     """Parse lead_time string to get hours"""
  35.     # Format: 'X days HH:MM:SS'
  36.     parts = lead_time_str.strip().split()
  37.     days = int(parts[0])
  38.     time_part = parts[2]  # HH:MM:SS
  39.     hours, minutes, seconds = map(int, time_part.split(':'))
  40.     total_hours = days * 24 + hours + minutes / 60 + seconds / 3600
  41.     return total_hours

  42. def simplify_track_id(track_id):
  43.     """Simplify track ID (e.g., AL052025 -> 05A, WP162025 -> 16W)"""
  44.     if len(track_id) >= 8:
  45.         basin = track_id[0]  # First letter
  46.         number = track_id[2:4]  # First two numbers
  47.         return f"{number}{basin}"
  48.     return track_id

  49. def draw_wind_radii(ax, lat, lon, ne_radius, se_radius, sw_radius, nw_radius, color, zorder=15):
  50.     """Draw wind radii for four quadrants"""
  51.     # Convert 0km to 0.01km for robustness
  52.     radii = [max(0.01, r) for r in [ne_radius, se_radius, sw_radius, nw_radius]]
  53.    
  54.     # Check if all radii are effectively 0
  55.     if all(r <= 0.01 for r in radii):
  56.         return
  57.    
  58.     # Convert km to degrees (approximate)
  59.     km_to_deg = 1/111.32
  60.     radii_deg = [r * km_to_deg for r in radii]
  61.    
  62.     # Define angles for each quadrant
  63.     angles = {
  64.         'NE': np.linspace(0, np.pi/2, 50),      # 0 to 90 degrees
  65.         'SE': np.linspace(np.pi/2, np.pi, 50),   # 90 to 180 degrees
  66.         'SW': np.linspace(np.pi, 3*np.pi/2, 50), # 180 to 270 degrees
  67.         'NW': np.linspace(3*np.pi/2, 2*np.pi, 50) # 270 to 360 degrees
  68.     }
  69.    
  70.     # Calculate points for each quadrant
  71.     all_x, all_y = [], []
  72.    
  73.     for i, (quadrant, theta_range) in enumerate(angles.items()):
  74.         radius = radii_deg[i]
  75.         x_quad = lon + radius * np.cos(theta_range)
  76.         y_quad = lat + radius * np.sin(theta_range)
  77.         all_x.extend(x_quad)
  78.         all_y.extend(y_quad)
  79.    
  80.     # Close the polygon
  81.     if all_x and all_y:
  82.         all_x.append(all_x[0])
  83.         all_y.append(all_y[0])
  84.         ax.plot(all_x, all_y, color=color, linewidth=1.0,
  85.                 transform=ccrs.PlateCarree(), zorder=zorder)

  86. def create_smooth_track(hours, lons, lats, points_per_segment=20):
  87.     """Create smooth track using interpolation"""
  88.     if len(hours) < 2:
  89.         return lons, lats
  90.    
  91.     # Create interpolated points
  92.     interp_hours = []
  93.     for i in range(len(hours)-1):
  94.         segment = np.linspace(hours[i], hours[i+1], num=points_per_segment)[:-1]
  95.         interp_hours.extend(segment)
  96.     interp_hours.append(hours[-1])
  97.     interp_hours = np.array(interp_hours)
  98.    
  99.     # Use PCHIP interpolation for smooth curves
  100.     smooth_lons = PchipInterpolator(hours, lons)(interp_hours)
  101.     smooth_lats = PchipInterpolator(hours, lats)(interp_hours)
  102.    
  103.     return smooth_lons, smooth_lats

  104. def create_typhoon_map(df_typhoon, track_id, init_time_str):
  105.    
  106.     global textPairedContent
  107.     textPairedContent = textPairedContent + simplify_track_id(track_id) + ','
  108.    
  109.     """Create typhoon forecast map for a single typhoon"""
  110.     # Filter data for forecast times up to 120 hours
  111.     df_filtered = df_typhoon[df_typhoon['lead_time_hours'] <= 120].copy()
  112.    
  113.     # Only plot specific time points: 0, 12, 24, 36, 48, 72, 120 hours
  114.     target_hours = [0, 12, 24, 36, 48, 72, 120]
  115.     df_plot = df_filtered[df_filtered['lead_time_hours'].isin(target_hours)].copy()
  116.     df_plot = df_plot.sort_values('lead_time_hours')
  117.    
  118.     if df_plot.empty:
  119.         print(f"No data to plot for typhoon {track_id}")
  120.         return
  121.    
  122.     # Calculate map extent
  123.     lats = df_plot['lat'].values
  124.     lons = df_plot['lon'].values
  125.    
  126.     lat_margin = 8
  127.     lon_margin = 8
  128.    
  129.     extendLatMin = max(-90, np.min(lats) - lat_margin)
  130.     extendLatMax = min(90, np.max(lats) + lat_margin)
  131.     extendLonMin = max(-180, np.min(lons) - lon_margin)
  132.     extendLonMax = min(180, np.max(lons) + lon_margin)
  133.    
  134.     centerlon = (extendLonMin + extendLonMax) / 2
  135.    
  136.     # Create the map
  137.     myproj = ccrs.PlateCarree()
  138.     fig = plt.figure(figsize=(12.5, 11))
  139.     ax = fig.add_axes([0.01, 0.01, 0.9, 0.9], projection=ccrs.PlateCarree(central_longitude=centerlon))
  140.    
  141.     extend = [extendLonMin, extendLonMax, extendLatMin, extendLatMax]
  142.     ax.set_extent(extend, crs=ccrs.Geodetic())
  143.     ax.set_adjustable('datalim')
  144.    
  145.     # Add gridlines
  146.     gl = ax.gridlines(draw_labels=True, linewidth=0.3, color='k', alpha=0.65, linestyle=':', zorder=90)
  147.     gl.xformatter = LONGITUDE_FORMATTER
  148.     gl.yformatter = LATITUDE_FORMATTER
  149.     gl.xlocator = mticker.FixedLocator(np.arange(-180, 180, 5))
  150.     gl.ylocator = mticker.FixedLocator(np.arange(-90, 90, 5))
  151.     gl.top_labels = False
  152.     gl.right_labels = False
  153.     gl.xlines = True
  154.     gl.ylines = True
  155.     gl.xlabel_style = {'size': 10}
  156.     gl.ylabel_style = {'size': 10}
  157.    
  158.     # Add map features
  159.     ax.add_feature(cfeature.COASTLINE.with_scale('50m'), edgecolor='#6b6b6b', facecolor='none', lw=0.42)
  160.     ax.add_feature(cfeature.OCEAN.with_scale('50m'), facecolor='white')
  161.     ax.add_feature(cfeature.LAND.with_scale('50m'), facecolor='#e3e3e3')
  162.    
  163.     # Add shapefiles (if they exist)
  164.     shapefiles = [
  165.     ]
  166.    
  167.     for shapefile in shapefiles:
  168.         if os.path.exists(shapefile):
  169.             try:
  170.                 ax.add_geometries(Reader(shapefile).geometries(),
  171.                                 crs=myproj, facecolor='none', edgecolor='k', linewidth=0.80, zorder=10)
  172.             except Exception as e:
  173.                 print(f"Warning: Could not load shapefile {shapefile}: {e}")
  174.    
  175.     # Draw error circles first (lower zorder)
  176.     hours = df_plot['lead_time_hours'].values
  177.     fore_lon = df_plot['lon'].values
  178.     fore_lat = df_plot['lat'].values
  179.    
  180.     # Calculate error radius: 50km per 12 hours, convert to degrees
  181.     fore_r = (hours / 12) * 50 / 111.32  # 50km per 12 hours, convert km to degrees
  182.    
  183.     print(f"Debug: Error radii for {track_id}: {fore_r}")  # Debug info
  184.    
  185.     if len(hours) > 1:
  186.         # Interpolate for smooth error envelope
  187.         points_num = 12
  188.         interp_hours = []
  189.         for i in range(len(hours)-1):
  190.             segment = np.linspace(hours[i], hours[i+1], num=points_num)[:-1]
  191.             interp_hours.extend(segment)
  192.         interp_hours.append(hours[-1])
  193.         interp_hours = np.array(interp_hours)
  194.         
  195.         x = PchipInterpolator(hours, fore_lon)(interp_hours)
  196.         y = PchipInterpolator(hours, fore_lat)(interp_hours)
  197.         r = PchipInterpolator(hours, fore_r)(interp_hours)
  198.         
  199.         # Create error envelope
  200.         thetas = np.linspace(0, 2 * np.pi, 360)
  201.         polygon_x = x[:,None] + r[:,None] * np.cos(thetas)  # Note: cos for longitude
  202.         polygon_y = y[:,None] + r[:,None] * np.sin(thetas)  # Note: sin for latitude
  203.         
  204.         ps = [Polygon(i) for i in np.dstack((polygon_x, polygon_y))]
  205.         n = range(len(ps)-1)
  206.         if n:  # Only if there are at least 2 circles
  207.             convex_hulls = [MultiPolygon([ps[i], ps[i+1]]).convex_hull for i in n]
  208.             polygons = unary_union(convex_hulls)
  209.             
  210.             ax.add_geometries([polygons], ccrs.PlateCarree(),
  211.                              facecolor='yellow', alpha=0.5, edgecolor='black', linewidth=0.5, zorder=5)
  212.             print(f"Added error envelope for {track_id}")
  213.         else:
  214.             # If only one point, draw a simple circle
  215.             if len(hours) == 1 and fore_r[0] > 0:
  216.                 circle = plt.Circle((fore_lon[0], fore_lat[0]), fore_r[0],
  217.                                   facecolor='yellow', alpha=0.5, edgecolor='black',
  218.                                   linewidth=0.5, transform=ccrs.PlateCarree(), zorder=5)
  219.                 ax.add_patch(circle)
  220.                 print(f"Added single error circle for {track_id}")
  221.     else:
  222.         print(f"Not enough points for error envelope: {len(hours)} points")
  223.    
  224.     # ===== 修改部分:绘制平滑的台风路径曲线 =====
  225.     track_lons = df_plot['lon'].values
  226.     track_lats = df_plot['lat'].values
  227.     track_hours = df_plot['lead_time_hours'].values
  228.    
  229.     # 创建平滑曲线
  230.     if len(track_hours) >= 2:
  231.         smooth_lons, smooth_lats = create_smooth_track(track_hours, track_lons, track_lats)
  232.         # 绘制平滑的路径曲线
  233.         ax.plot(smooth_lons, smooth_lats, color='hotpink', linewidth=1.1,
  234.                 transform=ccrs.PlateCarree(), zorder=20, label='Track')
  235.         print(f"Drew smooth track with {len(smooth_lons)} interpolated points")
  236.     else:
  237.         # 如果只有一个点,绘制原来的直线
  238.         ax.plot(track_lons, track_lats, color='hotpink', linewidth=1.1,
  239.                 transform=ccrs.PlateCarree(), zorder=20, label='Track')
  240.         print(f"Drew straight track (insufficient points for smoothing)")
  241.    
  242.     global_max_34kt = max(
  243.         df_plot['radius_34_knot_winds_ne_km'].max(),
  244.         df_plot['radius_34_knot_winds_se_km'].max(),
  245.         df_plot['radius_34_knot_winds_sw_km'].max(),
  246.         df_plot['radius_34_knot_winds_nw_km'].max()
  247.     )
  248.    
  249.     prev_lon, prev_lat = None, None  # 记录上一点
  250.     # Draw typhoon positions and wind radii
  251.     for idx, row in df_plot.iterrows():
  252.         lat, lon = row['lat'], row['lon']
  253.         
  254.         # Draw position marker
  255.         if row['lead_time_hours'] == 0:
  256.             # 第一个点用黑色'x'标记,加粗显示
  257.             ax.plot(lon, lat, 'x', color='black', markersize=5.5,
  258.                     markeredgewidth=1, transform=ccrs.PlateCarree(), zorder=25)
  259.         else:
  260.             # 其他点用粉色'o'标记
  261.             ax.plot(lon, lat, 'o', color='hotpink', markersize=3.5,
  262.                     transform=ccrs.PlateCarree(), zorder=25)
  263.         
  264.         # Draw wind radii (34, 50, 64 knots)
  265.         wind_data = [
  266.             # 34-knot winds (7级风圈) - pink
  267.             (row['radius_34_knot_winds_ne_km'], row['radius_34_knot_winds_se_km'],
  268.              row['radius_34_knot_winds_sw_km'], row['radius_34_knot_winds_nw_km'], 'hotpink'),
  269.             # 50-knot winds (10级风圈) - dark red
  270.             (row['radius_50_knot_winds_ne_km'], row['radius_50_knot_winds_se_km'],
  271.              row['radius_50_knot_winds_sw_km'], row['radius_50_knot_winds_nw_km'], 'darkred'),
  272.             # 64-knot winds (12级风圈) - red
  273.             (row['radius_64_knot_winds_ne_km'], row['radius_64_knot_winds_se_km'],
  274.              row['radius_64_knot_winds_sw_km'], row['radius_64_knot_winds_nw_km'], 'red')
  275.         ]
  276.         
  277.         for ne, se, sw, nw, color in wind_data:
  278.             if pd.notna(ne) and pd.notna(se) and pd.notna(sw) and pd.notna(nw):
  279.                 draw_wind_radii(ax, lat, lon, ne, se, sw, nw, color)
  280.         

  281.         # 获取风速值并转换为整数
  282.         # 解析valid_time获取日期和小时
  283.         try:
  284.             # 解析valid_time字符串(格式:'2025-08-12 06:00:00')
  285.             valid_time_str = row['valid_time']
  286.             
  287.             # 直接提取月份中的日和小时部分
  288.             # 格式:'YYYY-MM-DD HH:MM:SS'
  289.             parts = valid_time_str.split()
  290.             date_part = parts[0]  # '2025-08-12'
  291.             time_part = parts[1]  # '06:00:00'
  292.             
  293.             # 提取日和小时
  294.             day = date_part.split('-')[2]  # 从'2025-08-12'提取'12'
  295.             hour = time_part.split(':')[0]  # 从'06:00:00'提取'06'
  296.             
  297.             # 格式化日期标签:日/小时Z
  298.             date_label = f"{day}/{hour}Z"
  299.         except Exception as e:
  300.             print(f"Error parsing valid_time: {valid_time_str}, using fallback. Error: {e}")
  301.             # 如果解析失败,使用备用标签(小时数)
  302.             date_label = f"{int(row['lead_time_hours'])}h"
  303.         
  304.         # 获取风速值并转换为整数
  305.         try:
  306.             wind_speed = int(row['maximum_sustained_wind_speed_knots'])
  307.         except (ValueError, TypeError):
  308.             wind_speed = "N/A"
  309.         
  310.         # 计算动态c_r = 最大34kt风圈半径(转换为地理坐标偏移量)
  311.         c_r = global_max_34kt/ 111.32  # 约1°纬度=111.32km
  312.         global_max_34kt = global_max_34kt + 10
  313.         offset_deg = c_r * 1.5  # 维持原有的偏移系数
  314.    
  315.         # 计算连线方向
  316.         if prev_lon is not None and prev_lat is not None:
  317.             dx = lon - prev_lon
  318.             dy = lat - prev_lat
  319.    
  320.             # 判定斜向
  321.             if abs(dx) < 1e-6 or abs(dy) < 1e-6:
  322.                 # 水平或竖直
  323.                 angle_deg = 30
  324.             else:
  325.                 if dx > 0 and dy < 0:   # 左上到右下
  326.                     angle_deg = 30
  327.                 elif dx > 0 and dy > 0: # 左下到右上
  328.                     angle_deg = -30
  329.                 else:
  330.                     # 其它情况(比如从右到左),也可以用默认值
  331.                     angle_deg = 30
  332.         else:
  333.             # 第一个点
  334.             angle_deg = 30
  335.    
  336.         # 偏移计算
  337.         theta = np.radians(angle_deg)
  338.         dlat = offset_deg * np.cos(theta)
  339.         dlon = (offset_deg * np.sin(theta)) / np.cos(np.radians(lat))
  340.    
  341.         label_lon = lon + dlon
  342.         label_lat = lat + dlat
  343.    
  344.         ax.text(label_lon, label_lat, f"{date_label} {wind_speed}KTS",
  345.                 transform=ccrs.PlateCarree(), fontsize=9, ha='left', va='bottom', zorder=30)
  346.         ax.plot([lon, label_lon], [lat, label_lat], color='black', linewidth=0.6,
  347.                 transform=ccrs.PlateCarree(), zorder=29)
  348.    
  349.         prev_lon, prev_lat = lon, lat
  350.    
  351.     # Add title and legend
  352.     simplified_id = simplify_track_id(track_id)
  353.     plt.title(f'FNV3 Ensemble Mean Track Forecast for Tropical Cyclone [{simplified_id}]\nInitTime: {init_time_str}',
  354.               fontsize=14, fontweight='bold')
  355.    
  356.     # Add legend for wind radii
  357.     legend_elements = [
  358.         plt.Line2D([0], [0], color='hotpink', linewidth=2, label='34kt winds'),
  359.         plt.Line2D([0], [0], color='darkred', linewidth=2, label='50kt winds'),
  360.         plt.Line2D([0], [0], color='red', linewidth=2, label='64kt winds'),
  361.         patches.Rectangle((0, 0), 1, 1, facecolor='yellow', alpha=0.3, label='Error envelope')
  362.     ]
  363.     ax.legend(handles=legend_elements, loc='upper right', bbox_to_anchor=(0.991, 0.991))
  364.     plt.figtext(0.775, 0.0276, "Copyright\xa9 SMCA", fontsize = 11,color="black",zorder=101)
  365.    
  366.     # Save the figure
  367.     filename = f"{simplified_id}_paired_{init_time_str[:-1]}.png"
  368.     filepath = f"/NWP/AI_TC_pic/{init_time_str[:-1]}/{filename}"
  369.     plt.savefig(filepath, dpi=300, bbox_inches='tight')
  370.    
  371.    
  372.     plt.close()
  373.    

  374. import os
  375. import pandas as pd

  376. textPairedContent = ''

  377. def main():
  378.     # 定义目录路径 将FNV3的文件放在该目录下即可 例如 FNV3_2025_08_13T00_00_paired.csv
  379.     directory = "/NWP/AI_TC_ENS/FNV3_paired_circle/"
  380.    
  381.     try:
  382.         # 获取目录下所有CSV文件
  383.         csv_files = [f for f in os.listdir(directory) if f.endswith('.csv')]
  384.         
  385.         if not csv_files:
  386.             print("No CSV files found in the directory.")
  387.             return
  388.         
  389.         print(f"Found {len(csv_files)} CSV files to process.")
  390.         
  391.         # 处理每个CSV文件
  392.         for csv_file in csv_files:
  393.             file_path = os.path.join(directory, csv_file)
  394.             print(f"\nProcessing file: {csv_file}")
  395.             
  396.             
  397.             try:
  398.                 init_time = extract_init_time(csv_file)  # 修改点:从文件名获取
  399.                 # 读取CSV文件,跳过前6行
  400.                 df = pd.read_csv(file_path, skiprows=6)
  401.                
  402.                 # 解析lead_time获取小时数
  403.                 df['lead_time_hours'] = df['lead_time'].apply(parse_lead_time)
  404.                
  405.                 # 获取唯一的台风track_id
  406.                 unique_typhoons = df['track_id'].unique()
  407.                
  408.                 print(f"Found {len(unique_typhoons)} typhoons in this file: {unique_typhoons}")
  409.                
  410.                 # 处理每个台风
  411.                 for track_id in unique_typhoons:
  412.                     df_typhoon = df[df['track_id'] == track_id].copy()
  413.                     
  414.                     # 获取init_time(同一台风的所有行应该相同)
  415.                     
  416.                     
  417.                     print(f"Processing typhoon {track_id} with {len(df_typhoon)} data points")
  418.                     
  419.                     # 为这个台风创建地图
  420.                     create_typhoon_map(df_typhoon, track_id, init_time)
  421.                
  422.                 print(f"Finished processing file: {csv_file}")
  423.                
  424.                
  425.             except Exception as e:
  426.                 print(f"Error processing file {csv_file}: {e}")
  427.                 import traceback
  428.                 traceback.print_exc()
  429.                 continue  # 继续处理下一个文件
  430.         
  431.         print("\n全部台风路径文件制作完成")
  432.         
  433.     except Exception as e:
  434.         print(f"Error accessing directory: {e}")
  435.         import traceback
  436.         traceback.print_exc()

  437. if __name__ == "__main__":
  438.     main()
复制代码


爱丽丝女儿镇楼

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

相信的心是你的魔法
SMCA小破站:www.smca.fun
喜欢制作好看的图片,如果能得到你的喜欢就很开心啦

1

主题

138

回帖

305

积分

热带低压

积分
305
 楼主| 发表于 2025-8-13 20:33 | 显示全部楼层
本帖最后由 菜园子 于 2025-8-13 20:51 编辑

效果图如下,因为刚做出来,还比较粗糙,你们可以试试进行美化,砸门相互学习,相互借鉴

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×

相信的心是你的魔法
SMCA小破站:www.smca.fun
喜欢制作好看的图片,如果能得到你的喜欢就很开心啦

1

主题

138

回帖

305

积分

热带低压

积分
305
 楼主| 发表于 2025-8-13 20:37 | 显示全部楼层

相信的心是你的魔法
SMCA小破站:www.smca.fun
喜欢制作好看的图片,如果能得到你的喜欢就很开心啦

3

主题

61

回帖

418

积分

热带低压

积分
418
发表于 2025-8-13 21:44 | 显示全部楼层
学习一下,感觉cartopy比自己从自然地球导数据要方便

点评

代码写的比较随意,有些投机取巧的地方,可以加以改进  发表于 2025-8-14 00:26
看不懂报文?试试这个:ZCZCNNNN.com

11

主题

2127

回帖

3883

积分

台风

积分
3883
发表于 2025-8-14 00:57 | 显示全部楼层
滋磁公开教程(本来我也不会画误差x
Viva la Laniakea!

1

主题

138

回帖

305

积分

热带低压

积分
305
 楼主| 发表于 2025-8-14 01:15 | 显示全部楼层
Enceladus 发表于 2025-8-14 00:57
滋磁公开教程(本来我也不会画误差x

其实是比较早的功能了,在Q群里相互毒奶的时候用的XD

相信的心是你的魔法
SMCA小破站:www.smca.fun
喜欢制作好看的图片,如果能得到你的喜欢就很开心啦

11

主题

2127

回帖

3883

积分

台风

积分
3883
发表于 2025-8-14 02:55 | 显示全部楼层
现学现用(

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
Viva la Laniakea!

1

主题

138

回帖

305

积分

热带低压

积分
305
 楼主| 发表于 2025-8-14 09:33 | 显示全部楼层

好好好,配色做的也很好看

相信的心是你的魔法
SMCA小破站:www.smca.fun
喜欢制作好看的图片,如果能得到你的喜欢就很开心啦
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|TY_Board论坛

GMT+8, 2025-8-15 13:49 , Processed in 0.052317 second(s), 20 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表