乐于分享
好东西不私藏

【p5js:花开有时】源码分享

【p5js:花开有时】源码分享
 一个基于 p5.js 的生成艺术 sketch:两朵数据花从同一片根簇中生长、开花、凋零,并由数字、花托、HUD 标尺与能量脉冲共同构成可视化生命系统。  

A generative art sketch in p5.js: two data flowers grow, bloom, and decay from a shared root cluster, surrounded by numeric calyxes, HUD rulers, and pulsing data annotations.”

项目下载

https://github.com/KesshoDew/Flowers-Bloom-and-Fade

项目视频
已关注
关注
重播 分享
项目说明

Data Bloom v1.0 / 数据开花


作品概述 / Overview

Data Bloom v1.0将花朵生长过程转译为一套数据化视觉语言。花茎像白色线性轨迹一样从画布下方升起,花朵由程序化三维曲面生成,茎上的数字随生长从下往上出现,并在凋零时自上而下归零消失。

Data Bloom v1.0translates the growth of flowers into a data-driven visual language. White linear stems rise from below the canvas, procedural 3D flowers unfold from their tips, numeric labels appear upward along the stems, and drain back to zero during decay.

画面中的主要层次 / Main visual layers:

  1. 共享根簇 / Shared root cluster   两朵花从画布下方相近但略微错开的起点生长,形成同源但不完全重合的根部关系。   Two flowers grow from nearby, slightly offset origins below the canvas, creating a shared but not identical root system.
  2. 线性花茎 / Linear stems   花茎由分段三维曲线构成,包含自然弯曲、风摆、能量脉冲、倒伏和分段消失。   The stems are segmented 3D curves with natural bending, wind sway, energy pulses, collapse, and top-down dissolution.
  3. 程序化花朵 / Procedural flowers   花朵不依赖外部模型,由参数化曲面生成。每次运行可随机生成不同的花头尺寸、比例、朝向、灰度和茎形。   The flowers are generated as parametric surfaces with no external model files. Each run can randomize flower scale, proportion, orientation, grayscale, and stem shape.
  4. 数字花托 / Numeric calyx   茎顶和花底之间叠加密集的“数字 + 黑框”,模拟花托结构,也让花与茎的衔接更自然。   Dense black-box numeric labels around the stem tip mimic a calyx and visually bridge the stem-flower connection.
  5. HUD 分析层 / HUD analysis layer   白色标尺、框选和数据标注会跟随花、茎、叶、根的状态出现,并与生命周期同步淡入、停留和淡出。   White rulers, selection frames, and data labels follow the flower, stem, leaf, and root states, fading in, holding, and fading out with the lifecycle.

交互方式 / Interaction

操作 / Action
效果 / Effect
鼠标左右拖动 / Drag horizontally
旋转整组花丛的观察视角 / Rotates the whole flower cluster view
刷新页面 / Refresh page
当 randomizeOnRun 为 true 时,生成新的花丛 / Generates a new flower cluster when randomizeOnRun is true

技术实现 / Technical Details

项目 / Item
说明 / Description
引擎 / Engine
p5.js v1.11.3,global mode / p5.js v1.11.3, global mode
画布尺寸 / Canvas
1440 x 1920 px,竖屏 / portrait
素材依赖 / Assets
无本地素材依赖,适合 p5.js Web Editor / No local assets, suitable for the p5.js Web Editor
花朵生成 / Flower generation
参数化三维曲面与缓存几何体 / Parametric 3D surfaces with cached geometry
生命周期 / Lifecycle
生长、开花、花瓣掉落、叶片掉落、茎倒伏、分段消失 / Growth, bloom, petal fall, leaf fall, stem collapse, segmented dissolution
节奏系统 / Rhythm system
旋转、花周期、脉冲、HUD、风摆使用整数倍关系同步 / Rotation, lifecycle, pulse, HUD, and wind are synchronized by integer rhythmic ratios
数字系统 / Numeric system
茎上数字、分支数字、数字花托均绘制在 2D 顶层 / Stem labels, branch labels, and numeric calyxes are drawn on a top 2D layer
HUD 系统 / HUD system
标尺、框选、数据标注绑定植物关键点 / Rulers, frames, and labels are bound to plant key points

文件结构 / Files

├── index.html   本地预览页面 / Local preview page└── sketch.js    p5.js Web Editor 可运行源码 / p5.js Web Editor-ready source

运行方式 / How to Run

方式一:直接打开 HTML / Option 1: Open HTML directly用浏览器打开 index.html,它会通过 CDN 加载 p5.js 并运行同目录下的 sketch.js。Open index.html in a browser. It loads p5.js from a CDN and runs sketch.js from the same folder.

方式二:p5.js Web Editor / Option 2: p5.js Web Editor将 sketch.js 的全部内容复制到editor.p5js.org中运行。Paste the full content of sketch.js intoeditor.p5js.org.


参数自定义 / Customization

所有可调参数集中在 sketch.js 顶部的 CONFIG 中。All adjustable parameters are centralized in CONFIG at the top of sketch.js.

参数组 / Parameter group
说明 / Description
canvasWidth
 / canvasHeight
画布尺寸 / Canvas size
flowerScale
花朵整体大小 / Overall flower scale
flowerCount
花朵数量,上限为 5 / Flower count, capped at 5
randomizeOnRun
每次运行是否随机生成 / Randomize on each run
flowerClusterSeed
固定生成时的花丛种子 / Flower cluster seed for fixed generation
flowerRootSpreadX
 / flowerRootSpreadZ
根部错开范围 / Root offset range
flowerClusterHeightStagger
花丛高低错落强度 / Vertical height staggering
rhythmBeatFrames
基础节拍帧数,也控制旋转一圈时长 / Base rhythm frames and one-rotation duration
rhythmLifecycleBeats
一个完整花周期包含的节拍数 / Beats per full lifecycle
rhythmPulseCyclesPerBeat
每圈旋转内的能量脉冲次数 / Pulse cycles per rotation
bloomStartStemGrowRatio
茎长到多少比例时花开始成长 / Stem-growth ratio at which the flower starts growing
calyxNumberCount
每朵花的数字花托密度 / Numeric calyx density per flower
stemLength
 / stemMainBend
花茎长度与主弯曲 / Stem length and main bend
leafLength
 / leafWidth
叶片尺寸 / Leaf size
numberPulseSpeed
数字脉冲速度,节奏同步开启时自动计算 / Number pulse speed, auto-calculated when rhythm sync is enabled
hudAlpha
 / hudStrokeWeight
HUD 清晰度 / HUD clarity
hudFadeOutStart
HUD 停留时间,越大停留越久 / HUD hold time, larger means longer
hudRulerCount
 / hudBoxCount / hudLabelCount
每株花的 HUD 元素数量 / HUD element count per flower

创作 / Credits

项目 / Item
内容 / Detail
概念与实现 / Concept & Implementation
知髓食乌冬
AI 辅助 / AI Assistance
Codex
技术框架 / Framework
p5.js
许可 / License
MIT License

"A flower is rendered as data, and data learns to bloom.""花被渲染成数据,而数据学会开花。"

源码分享
// ╔══════════════════════════════════════════════════════════╗// ║              Data Bloom v1.0 / 数据开花                   ║// ╚══════════════════════════════════════════════════════════╝// 一个可在 p5.js Web Editor 中直接运行的生成艺术 sketch。// 画面由两朵共享根簇的数据花组成:白色根茎、灰度花瓣、数字花托、// 能量脉冲、HUD 标尺与生命周期动画共同构成“数据生长”的视觉系统。//// 使用方式:// 1. 本地预览:打开同目录下的 index.html。// 2. p5.js Web Editor:复制本文件全部代码到 sketch.js 中运行。// 3. 不需要任何本地图片、模型或外部素材;花朵、叶片、数字与 HUD 均由代码生成。// ╔══════════════════════════════════════════════════════════╗// ║              全局参数 CONFIG                              ║// ╚══════════════════════════════════════════════════════════╝// 所有可调参数集中在这里。建议优先调整数量、节奏、尺寸、随机种子与 HUD 密度;// 下方函数区主要负责生成与动画逻辑,通常不需要修改。constCONFIG= {  // ╔══════════════════════════════════════════════════════════╗  // ║                    画布参数                              ║  // ╚══════════════════════════════════════════════════════════╝  // 画布尺寸  canvasWidth:1440,  canvasHeight:1920,  // 背景颜色,0 为纯黑  backgroundColor:0,  // ╔══════════════════════════════════════════════════════════╗  // ║              程序化花朵模型参数                           ║  // ╚══════════════════════════════════════════════════════════╝  // 花朵网格精度:数值越大越细腻,但性能开销也越大  rows:60,  cols:80,  // 花瓣数量  petals:12,  // 花朵基础直径  diameter:40,  // 花瓣向外延展长度  petalLength:60,  // 花瓣尖锐程度:越大越尖,越小越圆  petalSharpness:0.2,  // 花朵立体高度  flowerHeight:130,  // 曲率参数 1:控制花瓣整体向上拱起的形态  curvature1:0.8,  // 曲率参数 2:控制花瓣高度衰减  curvature2:0.2,  // 表面起伏强度  bumpiness:4,  // 表面起伏频率  bumps:2.5,  // 花朵层数:大于 1 时会形成多层旋转花瓣  turns:2,  // 多层花瓣之间的高度差  heightSpread:0.4,  // 整体缩放  flowerScale:1.72,  // 初始观察角度:90 度会让花瓣开口朝上、根部朝下  tiltX:90,  tiltZ:90,  // ╔══════════════════════════════════════════════════════════╗  // ║                    节奏同步参数                           ║  // ╚══════════════════════════════════════════════════════════╝  // 是否启用统一节奏同步;开启后会自动计算旋转、生命周期、脉冲、HUD、风摆速度  rhythmSyncEnabled:true,  // 基础节拍帧数:当前设为 180 帧,60fps 下约 3 秒;也是画面旋转一整圈的时间  rhythmBeatFrames:180,  // 一个完整花周期包含几个基础节拍;5 表示花从生长到凋零经历 5 圈旋转  rhythmLifecycleBeats:5,  // 每个基础节拍内数字/茎能量脉冲循环次数;2 表示每圈旋转有 2 次能量上行  rhythmPulseCyclesPerBeat:2,  // 每个基础节拍内 HUD 扫描循环次数;2 表示每圈旋转有 2 次 HUD 扫描  rhythmHudCyclesPerBeat:2,  // 每个基础节拍内风摆循环次数;1 表示风摆和旋转同拍  rhythmWindCyclesPerBeat:1,  // 画布视角自动旋转速度:节奏同步开启时由 rhythmBeatFrames 自动计算  autoSpinSpeed:2,  // ╔══════════════════════════════════════════════════════════╗  // ║              花丛构图与随机生成参数                       ║  // ╚══════════════════════════════════════════════════════════╝  // 鼠标左右拖动旋转灵敏度  dragRotateSensitivity:0.35,  // 花朵数量,上限为 5;每只花共享同一个根簇,但拥有随机参数和错开的周期  flowerCount:2,  // 每次运行是否随机生成;true 表示刷新页面会得到新的花丛、叶片、数字和 HUD  randomizeOnRun:true,  // 多花随机种子,改这个数会换一组花的高度、弯曲、大小和朝向  flowerClusterSeed:260730,  // 多只花的根部横向错开范围;数值小会像从同一个根簇长出  flowerRootSpreadX:86,  // 多只花的根部深度错开范围  flowerRootSpreadZ:34,  // 多只花的根部上下错开范围  flowerRootYJitter:36,  // 多只花的顶部横向展开范围  flowerClusterSpreadX:420,  // 多只花的顶部深度展开范围  flowerClusterSpreadZ:220,  // 多只花顶端高低错落强度,越大花丛越有层次  flowerClusterHeightStagger:520,  // 多只花顶端高度随机范围  flowerClusterTopYJitter:70,  // 多只花头向外错开的角度范围;保持较小可让花开口总体更朝上  flowerClusterYawSpread:12,  // 多只花头上下俯仰的随机范围;越小开口越朝上  flowerClusterPitchJitter:5,  // ╔══════════════════════════════════════════════════════════╗  // ║              黑白灰材质与光照参数                         ║  // ╚══════════════════════════════════════════════════════════╝  // 灰色花朵明暗范围  grayMin:0,  grayMax:255,  // 花朵和叶片明暗对比强度,越大黑白分界越硬  plantContrastPower:1.8,  // 光照强度  ambientLightValue:200,  directionalLightValue:210,  // ╔══════════════════════════════════════════════════════════╗  // ║              根茎与数字花托参数                           ║  // ╚══════════════════════════════════════════════════════════╝  // 根茎数量  stemCount:1,  // 根茎长度:根点会根据该值落在画布下方,调大后花茎更长  stemLength:1080,  // 根茎向外散开的幅度  stemSpread:95,  // 根茎弯曲幅度  stemCurveAmount:15,  // 根茎分段数:越高曲线越平滑  stemSegments:18,  // 根茎线条粗细  stemStrokeWeight:4,  // 根茎颜色  stemColor:245,  // 花朵根部与茎顶之间的短花托长度,让衔接更自然  flowerNeckLength:34,  // 花托线条粗细  flowerNeckStrokeWeight:3.2,  // 每朵花根部密集数字花托数量  calyxNumberCount:22,  // 数字花托围绕花茎顶端的横向半径  calyxNumberRadiusX:62,  // 数字花托围绕花茎顶端的纵向半径  calyxNumberRadiusY:48,  // 数字花托字体大小  calyxNumberTextSize:18,  // 数字花托黑框透明度  calyxNumberBoxAlpha:235,  // 共同根点距离画布底部的距离:正数表示在画布外下方  commonRootOutsideOffset:20,  // 花茎顶端位置:也是花朵底端的连接点  stemTopX:0,  stemTopY:-100,  stemTopZ:0,  // 花朵模型的底端局部 z 坐标:用于把花朵底端精确移动到花茎顶端  flowerBottomLocalZ:-199,  // 花茎主弯曲幅度:正数向右弯,负数向左弯  stemMainBend:135,  // 花茎二次弯曲幅度,用于制造更自然的 S 形  stemSecondaryBend:-20,  // 花朵受花茎顶端朝向影响的强度,0 不受影响,1 完全跟随  flowerFollowStemDirection:0.62,  // 共同根点的可视标记大小;设为 0 可隐藏  commonRootMarkerSize:8,  // ╔══════════════════════════════════════════════════════════╗  // ║                    叶片参数                               ║  // ╚══════════════════════════════════════════════════════════╝  // 花茎叶片数量  leafCount:2,  // 叶片在花茎上的随机种子,改这个数会换一组叶片位置  leafSeed:486,  // 叶片长度  leafLength:200,  // 叶片宽度  leafWidth:100,  // 叶片分支长度占叶片长度的比例  leafBranchLengthRatio:0.36,  // 叶片连接处分支向上托举强度,越大叶片越不容易下垂  leafConnectionLift:0.45,  // 叶片根部与分支末端的重叠长度,让连接更自然  leafBaseOverlap:18,  // 叶片灰度  leafGray:215,  // 叶片曲面长度分段,越大越细腻  leafRows:20,  // 叶片曲面宽度分段,越大越细腻  leafCols:14,  // 叶片向上拱起高度  leafArchHeight:34,  // 叶片横向卷曲程度  leafCurl:18,  // 叶片尖锐程度,越大越尖  leafSharpness:0.5,  // 叶片沿自身 X 轴旋转角度:90 度可让叶面朝上  leafFaceUpRotationX:90,  // 叶片沿自身 Y 轴的随机旋转范围,让叶片空间角度更自然  leafRandomRotationYMin:-180,  leafRandomRotationYMax:180,  // ╔══════════════════════════════════════════════════════════╗  // ║              茎上数字与能量脉冲参数                       ║  // ╚══════════════════════════════════════════════════════════╝  // 茎上数字标签的随机种子,改这个数会换一组数字  stemNumberSeed:260729,  // 主茎数字标签数量  stemNumberCount:22,  // 分支数字标签数量  branchNumberCount:4,  // 数字标签字体大小  numberTextSize:24,  // 数字标签黑色底框左右内边距  numberPaddingX:7,  // 数字标签黑色底框上下内边距  numberPaddingY:4,  // 数字黑框左侧距离茎的距离,越小越贴近花茎  numberOffsetFromStem:18,  // 数字能量脉冲速度,越大向上传输越快;节奏同步开启时自动计算  numberPulseSpeed:0.018,  // 数字能量脉冲宽度,越大同时放大的数字越多  numberPulseWidth:0.12,  // 数字能量脉冲最大放大倍数  numberPulseScale:1.1,  // 数字能量脉冲亮度增强  numberPulseBrightness:70,  // 数字能量脉冲对数值的推动强度,1 表示脉冲峰值可把数值推到 1  numberPulseValueBoost:0.85,  // 全株能量脉冲对花、叶、茎的亮度推动强度  plantPulseBrightness:115,  // 全株能量脉冲经过茎时额外增加的线宽  stemPulseWeightBoost:5,  // 凋零时数字开始从上往下归零的凋零进度  numberDrainStart:0.42,  // 凋零时数字全部归零消失的凋零进度  numberDrainEnd:0.9,  // 单个数字归零的过渡宽度,越小越像一格一格熄灭  numberDrainStepWidth:0.08,  // ╔══════════════════════════════════════════════════════════╗  // ║              生长、开花、凋零生命周期参数                 ║  // ╚══════════════════════════════════════════════════════════╝  // 生命周期总时长,单位为帧;节奏同步开启时由 rhythmBeatFrames 和 rhythmLifecycleBeats 自动计算  lifecycleFrames:720,  // 幼苗到主茎基本长成的时间点,0 到 1  sproutEnd:0.34,  // 花朵开始成长时对应的茎生长比例;0.33 表示茎长到三分之一时花就开始成长  bloomStartStemGrowRatio:0.33,  // 花朵从花苞到盛开的时间点,0 到 1  bloomEnd:0.58,  // 开始凋零的时间点,0 到 1  wiltStart:0.72,  // 凋零时花朵向下垂落角度  wiltDroopAngle:58,  // 掉落花瓣的下落距离  fallingPetalDistance:520,  // 掉落花瓣左右飘散幅度  fallingPetalDrift:170,  // 掉落花瓣旋转速度  fallingPetalSpin:170,  // 掉落花瓣大小,相对花朵整体大小  fallingPetalScale:1.05,  // 掉落花瓣曲面长度分段,越小越流畅  fallingPetalRows:14,  // 掉落花瓣曲面宽度分段,越小越流畅  fallingPetalCols:10,  // 单片花瓣掉落持续时间,值越大越慢越舒缓  fallingPetalDuration:0.38,  // 花瓣消失开始进度  fallingPetalFadeStart:0.62,  // 叶子开始掉落的凋零进度,需晚于花瓣开始掉落  leafFallStart:0.46,  // 叶子掉落完成的凋零进度  leafFallEnd:0.78,  // 叶子掉落距离  fallingLeafDistance:420,  // 叶子掉落时左右飘移幅度  fallingLeafDrift:120,  // 茎开始倒伏的凋零进度  stemCollapseStart:0.68,  // 茎开始分段变黑消失的凋零进度;应晚于 stemCollapseStart  stemFadeStart:0.78,  // 茎完全分段消失的凋零进度  stemFadeEnd:1,  // 单段茎变黑消失的过渡宽度,越小越像逐段熄灭  stemSegmentFadeWidth:0.14,  // 凋零时额外增加的主弯曲幅度,让倒伏前先大幅度弯曲  stemWiltExtraBend:260,  // 茎倒伏方向与幅度  stemCollapseBend:360,  // 茎倒伏下沉距离  stemCollapseDrop:300,  // 数字跟随主茎生长同步出现:true 表示茎长到哪里,数字出现到哪里  numberSyncWithStemGrowth:true,  // ╔══════════════════════════════════════════════════════════╗  // ║                    风摆参数                               ║  // ╚══════════════════════════════════════════════════════════╝  // 风摆整体强度,0 表示关闭  windAmount:25,  // 风摆速度,越大摆动越快;节奏同步开启时自动计算  windSpeed:0.009,  // 风摆沿茎高度的传播相位,越大越有波浪感  windWave:1.8,  // 花朵额外点头幅度,让花头有轻微滞后感  flowerWindNod:2.2,  // 花朵沿自身 Y 轴的左右摇摆幅度  flowerWindSway:0.7,  // 数字在生长过程中出现的延迟  numberAppearStart:0.22,  // ╔══════════════════════════════════════════════════════════╗  // ║              HUD 标尺、框选与数据标注参数                 ║  // ╚══════════════════════════════════════════════════════════╝  // 是否显示白色装饰性 HUD 元素  hudEnabled:true,  // HUD 随机种子,改这个数会换一套标尺、框选、数据标注  hudSeed:431960,  // HUD 整体透明度  hudAlpha:165,  // HUD 线条粗细  hudStrokeWeight:1.35,  // HUD 动画速度;节奏同步开启时自动计算  hudAnimationSpeed:0.008,  // HUD 淡入结束位置,0 到 1;越小越快出现  hudFadeInEnd:0.14,  // HUD 淡出开始位置,0 到 1;越大停留越久  hudFadeOutStart:0.9,  // HUD 跟随植物关键点的强度,1 表示完全贴合植物,0 表示保持随机屏幕位置  hudFollowStrength:0.92,  // HUD 元素距离画布边缘的安全距离  hudMargin:90,  // 每株花的随机长度标尺数量  hudRulerCount:3,  // 每株花的框选标记数量  hudBoxCount:2,  // 每株花的数据标注数量  hudLabelCount:3,  // 标尺最短和最长长度  hudRulerMinLength:90,  hudRulerMaxLength:220,  // 框选标记最小和最大尺寸  hudBoxMinSize:90,  hudBoxMaxSize:220,  // HUD 角框线段长度  hudCornerLength:22,  // 数据标注连接线长度  hudLabelLineLength:44};// ╔══════════════════════════════════════════════════════════╗// ║                    全局运行状态                           ║// ╚══════════════════════════════════════════════════════════╝letflowerGeometry;letflowerPetalGeometries= [];letstemLeaves= [];letstemNumbers= [];letnumberAnchors= [];letnumberLayer;lethudLayer;lethudMarks= [];letflowerPlants= [];letcurrentPlant=null;letlifeState;letviewRotationY=0;letdragRotationY=0;letlastMouseX=0;letisDraggingFlower=false;letrunSeed=0;// ╔══════════════════════════════════════════════════════════╗// ║              初始化、随机种子与花丛生成                   ║// ╚══════════════════════════════════════════════════════════╝functionsetup() {  syncRhythmConfig();  initializeRunSeed();  createCanvas(CONFIG.canvasWidth, CONFIG.canvasHeight, WEBGL);  pixelDensity(1);  angleMode(DEGREES);  noStroke();  textFont("monospace");  numberLayer =createGraphics(CONFIG.canvasWidth, CONFIG.canvasHeight);  numberLayer.textFont("monospace");  hudLayer =createGraphics(CONFIG.canvasWidth, CONFIG.canvasHeight);  hudLayer.textFont("monospace");  makeFlowerGeometry();  makeFlowerPetalGeometries();  makeStemLeaves();  makeStemNumbers();  makeFlowerPlants();  makeHudMarks();}functionmakeFlowerPlants() {  flowerPlants = [];  randomSeed(getRuntimeSeed(CONFIG.flowerClusterSeed, 10000));  consttotal=constrain(floor(CONFIG.flowerCount), 15);  for (let i =0; i < total; i++) {    constratio= total ===1?0.5: i / (total -1);    constcentered= ratio -0.5;    constheightPattern=getFlowerClusterHeightPattern(i, total);    constleafSeedOffset=floor(random(1000));    constnumberSeedOffset=floor(random(1000));    constplant= {      index: i,      cycleOffsetFrames:round(i *CONFIG.rhythmBeatFrames),      rootX: centered *CONFIG.flowerRootSpreadX +random(-1414),      rootY:CONFIG.stemTopY +CONFIG.stemLength +CONFIG.commonRootOutsideOffset +random(-CONFIG.flowerRootYJitter, CONFIG.flowerRootYJitter),      rootZ:random(-CONFIG.flowerRootSpreadZ, CONFIG.flowerRootSpreadZ),      stemTopX:CONFIG.stemTopX + centered *CONFIG.flowerClusterSpreadX +random(-4646),      stemTopY:CONFIG.stemTopY + heightPattern *CONFIG.flowerClusterHeightStagger +random(-CONFIG.flowerClusterTopYJitter, CONFIG.flowerClusterTopYJitter),      stemTopZ:CONFIG.stemTopZ +random(-CONFIG.flowerClusterSpreadZ, CONFIG.flowerClusterSpreadZ),      stemMainBend:CONFIG.stemMainBend *random(0.651.2) + centered *90,      stemSecondaryBend:CONFIG.stemSecondaryBend *random(0.651.35),      stemCurveAmount:CONFIG.stemCurveAmount *random(0.71.5),      flowerScale:CONFIG.flowerScale *random(0.621.18),      flowerStretchX:random(0.721.32),      flowerStretchY:random(0.761.28),      flowerStretchZ:random(0.841.2),      flowerTiltX:CONFIG.tiltX +random(-CONFIG.flowerClusterPitchJitter, CONFIG.flowerClusterPitchJitter),      flowerTiltY: centered *CONFIG.flowerClusterYawSpread +random(-44),      flowerTiltZ:CONFIG.tiltZ + centered *72+random(-3636),      flowerSpin:random(0360),      flowerFollowStrength:CONFIG.flowerFollowStemDirection *random(0.781.08),      grayShift:random(-3434),      windPhase:random(0360),      leafSeedOffset,      numberSeedOffset    };    plant.leaves =createStemLeaves(leafSeedOffset);    plant.numbers =createStemNumbers(plant.leaves, numberSeedOffset);    flowerPlants.push(plant);  }}functiongetFlowerClusterHeightPattern(index, total) {  // 参考图中的花丛不是平均排布,而是高低穿插:高枝、中过渡、低枝同时存在。  constpatterns= {    1: [0],    2: [-0.420.34],    3: [-0.48, -0.080.42],    4: [-0.5, -0.180.240.48],    5: [-0.52, -0.18, -0.380.460.18]  };  return patterns[total][index];}functionsyncRhythmConfig() {  if (!CONFIG.rhythmSyncEnabled) return;  constbeatFrames=max(1, CONFIG.rhythmBeatFrames);  // 统一节奏关系:  // 1 个基础节拍 = 1 圈视角旋转;  // 花周期、能量脉冲、HUD 扫描、风摆都以它为整数倍/整除关系。  CONFIG.autoSpinSpeed =360/ beatFrames;  CONFIG.lifecycleFrames =round(beatFrames *CONFIG.rhythmLifecycleBeats);  CONFIG.numberPulseSpeed =CONFIG.rhythmPulseCyclesPerBeat / beatFrames;  CONFIG.hudAnimationSpeed =CONFIG.rhythmHudCyclesPerBeat / beatFrames;  CONFIG.windSpeed =CONFIG.rhythmWindCyclesPerBeat / beatFrames;}functioninitializeRunSeed() {  // 开启 randomizeOnRun 后,每次刷新页面都会生成新的运行时种子。  runSeed =CONFIG.randomizeOnRun    ?floor((Date.now() + Math.random() *999999) %100000000)    :0;}functiongetRuntimeSeed(fixedSeed, salt) {  // 关闭 randomizeOnRun 时使用固定种子,方便复现某一版构图。  returnCONFIG.randomizeOnRun ? runSeed + salt : fixedSeed;}// ╔══════════════════════════════════════════════════════════╗// ║                    主绘制循环                             ║// ╚══════════════════════════════════════════════════════════╝functiondraw() {  background(CONFIG.backgroundColor);  // 基础光照:保持黑白灰视觉,同时保留三维层次。  ambientLight(CONFIG.ambientLightValue);  directionalLight(    CONFIG.directionalLightValue,    CONFIG.directionalLightValue,    CONFIG.directionalLightValue,    -0.35,    0.55,    -1  );  // 旋转的是整株植物所在的场景视角,不是花朵单独自转。  viewRotationY +=CONFIG.autoSpinSpeed;  numberLayer.clear();  hudLayer.clear();  for (constplantof flowerPlants) {    currentPlant = plant;    lifeState =getLifeState(plant.cycleOffsetFrames);    push();    rotateY(viewRotationY + dragRotationY);    drawRootStems();    drawLeafBranches();    drawStemLeaves();    drawFallingLeaves();    drawFlowerAtStemTop();    collectStemNumberAnchors();    collectCalyxNumberAnchors();    pop();    drawStemNumbers();    drawDecorativeHud();  }  currentPlant =null;  compositeScreenLayer(numberLayer);  compositeScreenLayer(hudLayer);}functionmousePressed() {  lastMouseX = mouseX;  isDraggingFlower =true;}functionmouseDragged() {  if (!isDraggingFlower) return;  // 鼠标横向拖动控制整株植物的观察角度。  constdeltaX= mouseX - lastMouseX;  dragRotationY += deltaX *CONFIG.dragRotateSensitivity;  lastMouseX = mouseX;}functionmouseReleased() {  isDraggingFlower =false;}// ╔══════════════════════════════════════════════════════════╗// ║              生命周期、缓动与黑白对比                     ║// ╚══════════════════════════════════════════════════════════╝functiongetLifeState(offsetFrames=0) {  constlocalFrame= (frameCount - offsetFrames +CONFIG.lifecycleFrames *10) %CONFIG.lifecycleFrames;  constprogress= localFrame /CONFIG.lifecycleFrames;  conststemGrow=smoothStep(0, CONFIG.sproutEnd, progress);  constleafAppear=smoothStep(CONFIG.sproutEnd *0.48, CONFIG.bloomEnd, progress);  constbloomStart=CONFIG.sproutEnd *CONFIG.bloomStartStemGrowRatio;  constbloom=smoothStep(bloomStart, CONFIG.bloomEnd, progress);  constwilt=smoothStep(CONFIG.wiltStart, 1, progress);  constleafFall=smoothStep(CONFIG.leafFallStart, CONFIG.leafFallEnd, wilt);  conststemCollapse=smoothStep(CONFIG.stemCollapseStart, 1, wilt);  conststemFade=smoothStep(CONFIG.stemFadeStart, CONFIG.stemFadeEnd, wilt);  return {    progress,    stemGrow,    leafGrow: leafAppear,    flowerOpen: bloom,    wilt,    petalWilt: wilt,    leafFall,    stemCollapse,    stemAlpha:1- stemFade,    numberReveal:CONFIG.numberSyncWithStemGrowth      ? stemGrow      :smoothStep(CONFIG.numberAppearStart, CONFIG.bloomEnd, progress)  };}functionsmoothStep(edge0, edge1, value) {  constt=constrain((value - edge0) / (edge1 - edge0), 01);  return t * t * (3-2* t);}functiongetHighContrastPlantGray(grayValue) {  // 只用于花朵和叶片:把明暗范围压到极致,暗部为黑,亮部为白。  constnormalized=constrain(grayValue /25501);  constcontrasted=    normalized <0.5      ?0.5*pow(normalized *2, CONFIG.plantContrastPower)      :1-0.5*pow((1- normalized) *2, CONFIG.plantContrastPower);  returnround(contrasted *255);}// ╔══════════════════════════════════════════════════════════╗// ║              花朵几何体与花瓣缓存                         ║// ╚══════════════════════════════════════════════════════════╝functionmakeFlowerGeometry() {  beginGeometry();  for (let turn =0; turn <CONFIG.turns; turn++) {    constvertices= [];    constlayerHeight=CONFIG.flowerHeight * (1-CONFIG.heightSpread * turn /CONFIG.turns);    constlayerAngleOffset=90* turn /CONFIG.turns;    for (let theta =0; theta <CONFIG.rows; theta++) {      vertices.push([]);      for (let phi =0; phi <CONFIG.cols; phi++) {        constangle=360* phi / (CONFIG.cols -1);        constpetalWave=abs(sin(layerAngleOffset +CONFIG.petals /2* angle));        constradius=          (CONFIG.petalLength *pow(petalWave, CONFIG.petalSharpness) +CONFIG.diameter) *          theta /          CONFIG.rows;        constx= radius *cos(angle);        consty= radius *sin(angle);        constz=          vShape(layerHeight, radius /100, CONFIG.curvature1, CONFIG.curvature2, 1.5) -          200+          surfaceBump(CONFIG.bumpiness, radius /100, CONFIG.bumps, phi *360/CONFIG.cols);        vertices[theta].push(createVector(x, y, z));      }    }    drawFlowerSurface(vertices);  }  flowerGeometry =endGeometry();}functionmakeFlowerPetalGeometries() {  flowerPetalGeometries = [];  constpetalTotal=floor(CONFIG.petals);  for (let petalIndex =0; petalIndex < petalTotal; petalIndex++) {    beginGeometry();    for (let turn =0; turn <CONFIG.turns; turn++) {      constvertices= [];      constlayerHeight=CONFIG.flowerHeight * (1-CONFIG.heightSpread * turn /CONFIG.turns);      constlayerAngleOffset=90* turn /CONFIG.turns;      for (let theta =0; theta <CONFIG.rows; theta++) {        vertices.push([]);        for (let phi =0; phi <CONFIG.cols; phi++) {          constangle=360* phi / (CONFIG.cols -1);          constpetalWave=abs(sin(layerAngleOffset +CONFIG.petals /2* angle));          constradius=            (CONFIG.petalLength *pow(petalWave, CONFIG.petalSharpness) +CONFIG.diameter) *            theta /            CONFIG.rows;          vertices[theta].push(createVector(            radius *cos(angle),            radius *sin(angle),            vShape(layerHeight, radius /100, CONFIG.curvature1, CONFIG.curvature2, 1.5) -              200+              surfaceBump(CONFIG.bumpiness, radius /100, CONFIG.bumps, phi *360/CONFIG.cols)          ));        }      }      drawFlowerSurface(vertices, petalIndex);    }    flowerPetalGeometries.push(endGeometry());  }}functiondrawFlowerSurface(vertices, onlyPetalIndex=null) {  for (let theta =0; theta < vertices.length-1; theta++) {    constgrayValue=getHighContrastPlantGray(      map(theta, 0, vertices.length-1, CONFIG.grayMax, CONFIG.grayMin)    );    fill(grayValue);    for (let phi =0; phi < vertices[theta].length; phi++) {      constnextPhi= (phi +1) % vertices[theta].length;      constangle=360* phi / (CONFIG.cols -1);      if (onlyPetalIndex !==null&&getPetalIndexFromAngle(angle) !== onlyPetalIndex) continue;      consta= vertices[theta][phi];      constb= vertices[theta +1][phi];      constc= vertices[theta +1][nextPhi];      constd= vertices[theta][nextPhi];      beginShape();      vertex(a.x, a.y, a.z);      vertex(b.x, b.y, b.z);      vertex(c.x, c.y, c.z);      vertex(d.x, d.y, d.z);      endShape(CLOSE);    }  }}// ╔══════════════════════════════════════════════════════════╗// ║              根茎、叶片与数字数据                         ║// ╚══════════════════════════════════════════════════════════╝functiondrawRootStems() {  if (lifeState.stemGrow <=0.01return;  push();  noFill();  for (let i =0; i <CONFIG.stemCount; i++) {    for (let j =0; j <CONFIG.stemSegments; j++) {      constt0= (j /CONFIG.stemSegments) * lifeState.stemGrow;      constt1= ((j +1) /CONFIG.stemSegments) * lifeState.stemGrow;      constmidT= (t0 + t1) *0.5;      constvisible=getStemSegmentVisible(midT);      if (visible <=0.01continue;      conststemPoint0=getStemPoint(t0, i);      conststemPoint1=getStemPoint(t1, i);      constpulse=getEnergyPulseAt(midT);      constgrayValue=getPulsedPlantGray(getStemSegmentGray(midT), pulse);      strokeWeight(CONFIG.stemStrokeWeight + pulse *CONFIG.stemPulseWeightBoost);      stroke(grayValue, 255* visible);      line(stemPoint0.x, stemPoint0.y, stemPoint0.z, stemPoint1.x, stemPoint1.y, stemPoint1.z);    }  }  // 根点本身在画布外下方,通常不可见;当偏移较小时可作为白色起点标记出现。  if (CONFIG.commonRootMarkerSize >0) {    constrootPoint=getStemPoint(00);    push();    translate(rootPoint.x, rootPoint.y, rootPoint.z);    noStroke();    fill(CONFIG.stemColor);    circle(00, CONFIG.commonRootMarkerSize);    pop();  }  pop();}functionmakeStemLeaves() {  stemLeaves =createStemLeaves(0);}functioncreateStemLeaves(seedOffset) {  constleaves= [];  randomSeed(getRuntimeSeed(CONFIG.leafSeed, 20000) + seedOffset);  for (let i =0; i <CONFIG.leafCount; i++) {    leaves.push({      // 避开根部和花朵连接点,让叶片自然长在花茎中段。      t:random(0.320.72),      side: i %2===0?-1:1,      sizeScale:random(0.861.15),      angleJitter:random(-1212),      rotationY:random(CONFIG.leafRandomRotationYMin, CONFIG.leafRandomRotationYMax),      branchCurve:random(-11),      zLift:random(826)    });  }  return leaves;}functionmakeStemNumbers() {  stemNumbers =createStemNumbers(stemLeaves, 0);}functioncreateStemNumbers(leaves, seedOffset) {  constnumbers= [];  randomSeed(getRuntimeSeed(CONFIG.stemNumberSeed, 30000) + seedOffset);  for (let i =0; i <CONFIG.stemNumberCount; i++) {    constt= (i +0.5) /CONFIG.stemNumberCount;    numbers.push({      kind:"stem",      t,      value:randomStemNumber(),      side: i %2===0?-1:1    });  }  for (let leafIndex =0; leafIndex < leaves.length; leafIndex++) {    for (let i =0; i <CONFIG.branchNumberCount; i++) {      constt= (i +0.5) /CONFIG.branchNumberCount;      numbers.push({        kind:"branch",        leafIndex,        t,        value:randomStemNumber(),        side: i %2===0?-1:1      });    }  }  // 按数字所在的生长位置排序,让数字从下往上一个一个直接出现。  numbers.sort((a, b) =>getNumberLifeT(a, leaves) -getNumberLifeT(b, leaves));  for (let i =0; i < numbers.length; i++) {    numbers[i].revealIndex = i;  }  return numbers;}functiongetNumberLifeT(item, leaves=getActiveStemLeaves()) {  if (item.kind ==="branch") {    return leaves[item.leafIndex].t + item.t *0.08;  }  return item.t;}functiongetActiveStemLeaves() {  return currentPlant && currentPlant.leaves ? currentPlant.leaves : stemLeaves;}functiongetActiveStemNumbers() {  return currentPlant && currentPlant.numbers ? currentPlant.numbers : stemNumbers;}functiongetFlowerAttachmentT() {  // 花朵提前生长时,位置绑定当前茎尖;茎长满后自然固定在最终顶端。  returnconstrain(lifeState.stemGrow, 0.021);}functionrandomStemNumber() {  returnnf(random(0.010.99), 12);}// ╔══════════════════════════════════════════════════════════╗// ║              数字标签、花托数字与能量脉冲                 ║// ╚══════════════════════════════════════════════════════════╝functiongetTopDownDissolveProgress(t, start, end, widthValue) {  // t 越接近 1 越靠近花朵顶端;凋零时从上往下推进。  constphase=smoothStep(start, end, lifeState.wilt);  constpositionFromTop=1- t;  returnsmoothStep(positionFromTop, positionFromTop + widthValue, phase);}functiongetStemSegmentVisible(t) {  constdissolve=getTopDownDissolveProgress(    t,    CONFIG.stemFadeStart,    CONFIG.stemFadeEnd,    CONFIG.stemSegmentFadeWidth  );  return1-smoothStep(0.721, dissolve);}functiongetStemSegmentGray(t) {  constdissolve=getTopDownDissolveProgress(    t,    CONFIG.stemFadeStart,    CONFIG.stemFadeEnd,    CONFIG.stemSegmentFadeWidth  );  returnlerp(CONFIG.stemColor, 0, smoothStep(00.72, dissolve));}functiondrawStemNumbers() {  if (lifeState.numberReveal <=0.01|| lifeState.stemAlpha <=0.01return;  numberLayer.textFont("monospace");  numberLayer.textSize(CONFIG.numberTextSize);  numberLayer.textAlign(LEFT, CENTER);  numberLayer.rectMode(CORNER);  constactiveNumbers=getActiveStemNumbers();  constvisibleCount=ceil(lifeState.numberReveal * activeNumbers.length);  for (constanchorof numberAnchors) {    if (anchor.kind !=="calyx"&& anchor.revealIndex >= visibleCount) continue;    constpulse= anchor.kind ==="calyx"?getEnergyPulseAt(0.98) *0.45:getNumberPulse(anchor.revealIndex);    constdrain= anchor.kind ==="calyx"? lifeState.wilt :getNumberDrainProgress(anchor.revealIndex);    if (drain >=1continue;    constdisplayValue=getPulsedNumberValue(anchor.value, pulse, drain);    if (anchor.kind ==="calyx") {      drawCalyxNumberLabel(anchor.x, anchor.y, displayValue, pulse, anchor.alphaRatio * (1- drain));    } else {      drawNumberLabel(        anchor.x +CONFIG.numberOffsetFromStem,        anchor.y,        displayValue,        pulse,        1- drain      );    }  }}functioncollectStemNumberAnchors() {  numberAnchors = [];  for (constitemofgetActiveStemNumbers()) {    constlabelPoint=getStemNumberLabelPoint(item);    if (labelPoint.lifeT > lifeState.stemGrow) continue;    constscreenPoint=getScreenPoint(labelPoint);    numberAnchors.push({      x: screenPoint.x,      y: screenPoint.y,      value: item.value,      revealIndex: item.revealIndex    });  }}functioncollectCalyxNumberAnchors() {  if (lifeState.flowerOpen <=0.03|| lifeState.stemAlpha <=0.01return;  constattachmentT=getFlowerAttachmentT();  conststemTop=getStemPoint(attachmentT, 0);  constplantIndex= currentPlant ? currentPlant.index :0;  constappear=smoothStep(0.060.32, lifeState.flowerOpen);  consttangent=getStemTangent(attachmentT, 0);  consttangentAngle=atan2(tangent.y, tangent.x);  for (let i =0; i <CONFIG.calyxNumberCount; i++) {    constring= i /CONFIG.calyxNumberCount;    constangle= ring *360+ plantIndex *31;    constlayer= i %3;    constradiusScale=0.45+ layer *0.24;    constjitter=hash01(i *19.7+ plantIndex *83.1) -0.5;    constlocalX=cos(angle) *CONFIG.calyxNumberRadiusX * radiusScale + jitter *12;    constlocalY=sin(angle) *CONFIG.calyxNumberRadiusY * radiusScale -16+ layer *9;    constlocalZ=8+ layer *8;    constrotatedX= localX *cos(tangentAngle +90) - localY *sin(tangentAngle +90);    constrotatedY= localX *sin(tangentAngle +90) + localY *cos(tangentAngle +90);    constscreenPoint=getScreenPoint(createVector(stemTop.x + rotatedX, stemTop.y + rotatedY, stemTop.z + localZ));    numberAnchors.push({      kind:"calyx",      x: screenPoint.x,      y: screenPoint.y,      value:nf(hash01(i *47.3+ plantIndex *9.17), 12),      alphaRatio: appear    });  }}functiongetStemNumberLabelPoint(item) {  let point;  let lifeT;  if (item.kind ==="branch") {    constleaf=getActiveStemLeaves()[item.leafIndex];    point =getLeafBranchPoint(leaf, item.t);    lifeT = leaf.t;  } else {    point =getStemPoint(item.t, 0);    lifeT = item.t;  }  // 返回茎/分支自身的采样点;绘制时让黑框左侧贴近该点,排版更稳定。  constlabelPoint=createVector(point.x, point.y, point.z +6);  labelPoint.lifeT = lifeT;  return labelPoint;}functiongetEnergyPulseAt(position) {  // 凋零一开始就停止所有能量脉冲,随后进入归零和分段熄灭。  if (lifeState.wilt >0) return0;  constlocalFrame=getCurrentRhythmFrame();  constwaveHead= ((localFrame *CONFIG.numberPulseSpeed) %1+1) %1;  constdistance=abs(position - waveHead);  constwrappedDistance=min(distance, 1- distance);  constrawPulse=1-constrain(wrappedDistance /CONFIG.numberPulseWidth, 01);  // 平滑的单峰脉冲:能量经过时放大,离开后自然回落。  return rawPulse * rawPulse * (3-2* rawPulse);}functiongetNumberPulse(revealIndex) {  constcount=max(1, getActiveStemNumbers().length-1);  returngetEnergyPulseAt(revealIndex / count);}functiongetPulsedPlantGray(grayValue, pulse) {  returnmin(255, grayValue + pulse *CONFIG.plantPulseBrightness);}functiongetNumberDrainProgress(revealIndex) {  constcount=max(1, getActiveStemNumbers().length-1);  constposition= revealIndex / count;  constphase=smoothStep(CONFIG.numberDrainStart, CONFIG.numberDrainEnd, lifeState.wilt);  // revealIndex 越大越靠上;凋零时高处数字先减少到 0。  returnsmoothStep(1- position, 1- position +CONFIG.numberDrainStepWidth, phase);}functiongetPulsedNumberValue(valueText, pulse, drain=0) {  constbaseValue=constrain(parseFloat(valueText), 0.011);  constboostedValue= baseValue + (1- baseValue) * pulse *CONFIG.numberPulseValueBoost;  constdrainedValue=lerp(min(1, boostedValue), 0, drain);  // 生长时数值随脉冲向 1 靠近;凋零时从上到下依次减少到 0。  returnnf(constrain(drainedValue, 01), 12);}functiondrawNumberLabel(leftX, centerY, valueText, pulse=0, alphaRatio=1) {  constscaleValue=1+ pulse * (CONFIG.numberPulseScale -1);  constfontSize=CONFIG.numberTextSize * scaleValue;  numberLayer.textSize(fontSize);  constboxWidth= numberLayer.textWidth(valueText) +CONFIG.numberPaddingX *2* scaleValue;  constboxHeight= fontSize +CONFIG.numberPaddingY *2* scaleValue;  consttopY= centerY - boxHeight /2;  constbrightness=min(255185+ pulse *CONFIG.numberPulseBrightness);  numberLayer.noStroke();  numberLayer.fill(0230* alphaRatio);  numberLayer.rect(leftX, topY, boxWidth, boxHeight);  numberLayer.fill(brightness, 255* alphaRatio);  numberLayer.text(valueText, leftX +CONFIG.numberPaddingX * scaleValue, centerY -1);}functiondrawCalyxNumberLabel(centerX, centerY, valueText, pulse=0, alphaRatio=1) {  constscaleValue=1+ pulse *0.18;  constfontSize=CONFIG.calyxNumberTextSize * scaleValue;  numberLayer.textSize(fontSize);  numberLayer.textAlign(CENTER, CENTER);  constboxWidth= numberLayer.textWidth(valueText) +CONFIG.numberPaddingX *1.45* scaleValue;  constboxHeight= fontSize +CONFIG.numberPaddingY *1.65* scaleValue;  constleftX= centerX - boxWidth /2;  consttopY= centerY - boxHeight /2;  constbrightness=min(255205+ pulse *CONFIG.numberPulseBrightness);  numberLayer.noStroke();  numberLayer.fill(0, CONFIG.calyxNumberBoxAlpha * alphaRatio);  numberLayer.rect(leftX, topY, boxWidth, boxHeight);  numberLayer.fill(brightness, 255* alphaRatio);  numberLayer.text(valueText, centerX, centerY -1);  numberLayer.textAlign(LEFT, CENTER);}functionhash01(value) {  consthashed=sin(value) *43758.5453123;  return hashed -floor(hashed);}// ╔══════════════════════════════════════════════════════════╗// ║              HUD 标尺、框选与数据标注                     ║// ╚══════════════════════════════════════════════════════════╝functionmakeHudMarks() {  hudMarks = [];  randomSeed(getRuntimeSeed(CONFIG.hudSeed, 40000));  for (let i =0; i <CONFIG.hudRulerCount; i++) {    consthorizontal=random() >0.38;    hudMarks.push({      kind:"ruler",      x:random(CONFIG.hudMargin, width -CONFIG.hudMargin),      y:random(CONFIG.hudMargin, height -CONFIG.hudMargin),      length:random(CONFIG.hudRulerMinLength, CONFIG.hudRulerMaxLength),      horizontal,      tickCount:floor(random(513)),      phase:random(),      side:random() >0.5?1:-1,      target:random(["stemHeight""stemHeight""flowerWidth""leafSpan"])    });  }  for (let i =0; i <CONFIG.hudBoxCount; i++) {    constboxSize=random(CONFIG.hudBoxMinSize, CONFIG.hudBoxMaxSize);    hudMarks.push({      kind:"box",      x: width /2+random(-360360),      y: height /2+random(-620560),      w: boxSize *random(0.721.35),      h: boxSize *random(0.581.08),      phase:random(),      drift:random(-1010),      code:"SEL-"+nf(i +120),      target:random(["flower""stem""leaf""root"])    });  }  for (let i =0; i <CONFIG.hudLabelCount; i++) {    constside=random() >0.5?1:-1;    constvalue=random(0.010.99);    hudMarks.push({      kind:"label",      x: width /2+ side *random(250520),      y:random(CONFIG.hudMargin +80, height -CONFIG.hudMargin -140),      side,      phase:random(),      value:nf(value, 12),      code:random(["SIGNAL""TRACE""RANGE""SCAN""INDEX"]),      target:random(["growth""bloom""wilt""pulse""height""leaf"])    });  }}functiondrawDecorativeHud() {  if (!CONFIG.hudEnabled) return;  // 植物尚未开始生长时,HUD 不单独留存在画面中。  if (lifeState.stemGrow <=0.01|| lifeState.stemAlpha <=0.01return;  hudLayer.textFont("monospace");  hudLayer.textAlign(LEFT, CENTER);  hudLayer.rectMode(CORNER);  hudLayer.noFill();  hudLayer.strokeCap(SQUARE);  constcontext=getHudContext();  for (constmarkof hudMarks) {    constvisibility=getHudMarkPlantVisibility(mark, context);    if (visibility <=0.01continue;    constbasePulse=getHudPulse(mark.phase);    constpulse= {      phase: basePulse.phase,      alpha: basePulse.alpha * visibility,      reveal: basePulse.reveal,      scan: basePulse.scan    };    if (pulse.alpha <=0.01continue;    if (mark.kind ==="ruler") {      drawHudRuler(mark, pulse, context);    } elseif (mark.kind ==="box") {      drawHudBox(mark, pulse, context);    } elseif (mark.kind ==="label") {      drawHudLabel(mark, pulse, context);    }  }}functioncompositeScreenLayer(layer) {  push();  resetMatrix();  drawingContext.disable(drawingContext.DEPTH_TEST);  imageMode(CORNER);  image(layer, -width /2, -height /2);  drawingContext.enable(drawingContext.DEPTH_TEST);  pop();}functiongetHudPulse(phaseOffset) {  constlocalFrame=getCurrentRhythmFrame();  constphase= ((localFrame *CONFIG.hudAnimationSpeed + phaseOffset) %1+1) %1;  constfadeIn=smoothStep(0, CONFIG.hudFadeInEnd, phase);  constfadeOut=1-smoothStep(CONFIG.hudFadeOutStart, 1, phase);  constalpha= fadeIn * fadeOut;  constreveal=smoothStep(0.050.48, phase);  constscan=smoothStep(0.180.86, phase);  return { phase, alpha, reveal, scan };}functiongetHudContext() {  constroot=projectPlantPoint(getStemPoint(00));  constgrowT=constrain(lifeState.stemGrow, 0.0011);  constflowerT=getFlowerAttachmentT();  constcurrentTip=projectPlantPoint(getStemPoint(growT, 0));  conststemMid=projectPlantPoint(getStemPoint(growT *0.550));  constflower=projectPlantPoint(getStemPoint(flowerT, 0));  constflowerSize=lerp(70230, lifeState.flowerOpen) * (1- lifeState.wilt *0.28);  constplantHeight=max(80, abs(root.y - currentTip.y));  constlocalFrame=getCurrentRhythmFrame();  constenergyPosition= ((localFrame *CONFIG.numberPulseSpeed) %1+1) %1;  constenergyPoint=projectPlantPoint(getStemPoint(constrain(energyPosition, 0.02, growT), 0));  constleaves= [];  constactiveLeaves=getActiveStemLeaves();  for (let i =0; i < activeLeaves.length; i++) {    constleaf= activeLeaves[i];    if (lifeState.leafGrow <=0.01||getLeafFallProgress(i) >0.96continue;    leaves.push(projectPlantPoint(getLeafBranchPoint(leaf, lifeState.leafGrow)));  }  return {    root,    currentTip,    stemMid,    flower,    flowerSize,    plantHeight,    energyPoint,    leaves,    stemVisible:smoothStep(0.020.16, lifeState.stemGrow) * lifeState.stemAlpha,    flowerVisible:smoothStep(0.030.22, lifeState.flowerOpen) * (1-smoothStep(0.821, lifeState.wilt)),    leafVisible:smoothStep(0.030.2, lifeState.leafGrow) * (1- lifeState.leafFall),    pulseVisible:smoothStep(0.120.36, lifeState.stemGrow) * (1-smoothStep(00.04, lifeState.wilt))  };}functiongetHudMarkPlantVisibility(mark, context) {  // 每类 HUD 都必须依附到已经出现的植物部位,避免目标还没出现时停留在屏幕随机位置。  if (mark.kind ==="ruler") {    if (mark.target ==="flowerWidth"return context.flowerVisible;    if (mark.target ==="leafSpan"return context.leaves.length>0? context.leafVisible :0;    return context.stemVisible;  }  if (mark.kind ==="box") {    if (mark.target ==="flower"return context.flowerVisible;    if (mark.target ==="leaf"return context.leaves.length>0? context.leafVisible :0;    return context.stemVisible;  }  if (mark.kind ==="label") {    if (mark.target ==="bloom"return context.flowerVisible;    if (mark.target ==="leaf"return context.leaves.length>0? context.leafVisible :0;    if (mark.target ==="pulse"return context.pulseVisible;    if (mark.target ==="wilt") returnsmoothStep(0.020.18, lifeState.wilt) * lifeState.stemAlpha;    return context.stemVisible;  }  return context.stemVisible;}functionprojectPlantPoint(point) {  // HUD 是屏幕图层,这里用同一套 Y 轴旋转把植物空间点转成近似屏幕坐标。  constrotated=rotatePointAroundY(point, viewRotationY + dragRotationY);  returncreateVector(rotated.x + width /2, rotated.y + height /2);}functiongetCurrentRhythmFrame() {  return currentPlant ? frameCount - currentPlant.cycleOffsetFrames : frameCount;}functionmixHudPosition(screenX, screenY, targetX, targetY) {  returncreateVector(    lerp(screenX, targetX, CONFIG.hudFollowStrength),    lerp(screenY, targetY, CONFIG.hudFollowStrength)  );}functiondrawHudRuler(mark, pulse, context) {  constalphaValue=CONFIG.hudAlpha * pulse.alpha;  constanchor=getHudRulerAnchor(mark, context);  constlength=getHudRulerLength(mark, context);  constlengthNow= length * pulse.reveal;  consttickCount=max(2, mark.tickCount);  hudLayer.stroke(255, alphaValue);  hudLayer.strokeWeight(CONFIG.hudStrokeWeight);  if (anchor.horizontal) {    constx1= anchor.x - lengthNow /2;    constx2= anchor.x + lengthNow /2;    hudLayer.line(x1, anchor.y, x2, anchor.y);    for (let i =0; i <= tickCount; i++) {      consttickX=lerp(anchor.x - length /2, anchor.x + length /2, i / tickCount);      if (tickX < x1 || tickX > x2) continue;      consttickH= i %2===0?18:9;      hudLayer.line(tickX, anchor.y, tickX, anchor.y + tickH * mark.side);    }    constscanX=lerp(anchor.x - length /2, anchor.x + length /2, pulse.scan);    hudLayer.stroke(255, alphaValue *0.9);    hudLayer.line(scanX, anchor.y -12, scanX, anchor.y +12);  } else {    consty1= anchor.y - lengthNow /2;    consty2= anchor.y + lengthNow /2;    hudLayer.line(anchor.x, y1, anchor.x, y2);    for (let i =0; i <= tickCount; i++) {      consttickY=lerp(anchor.y - length /2, anchor.y + length /2, i / tickCount);      if (tickY < y1 || tickY > y2) continue;      consttickW= i %2===0?18:9;      hudLayer.line(anchor.x, tickY, anchor.x + tickW * mark.side, tickY);    }    constscanY=lerp(anchor.y - length /2, anchor.y + length /2, pulse.scan);    hudLayer.stroke(255, alphaValue *0.9);    hudLayer.line(anchor.x -12, scanY, anchor.x +12, scanY);  }}functiongetHudRulerAnchor(mark, context) {  let targetX = mark.x;  let targetY = mark.y;  let horizontal = mark.horizontal;  if (mark.target ==="stemHeight") {    targetX = context.stemMid.x -150* mark.side;    targetY = (context.root.y + context.currentTip.y) /2;    horizontal =false;  } elseif (mark.target ==="flowerWidth") {    targetX = context.flower.x;    targetY = context.flower.y - context.flowerSize *0.62;    horizontal =true;  } elseif (mark.target ==="leafSpan"&& context.leaves.length>0) {    constleaf= context.leaves[floor(abs(mark.phase *99)) % context.leaves.length];    targetX = leaf.x;    targetY = leaf.y;    horizontal =true;  }  constmixed=mixHudPosition(mark.x, mark.y, targetX, targetY);  mixed.x =constrain(mixed.x, CONFIG.hudMargin, width -CONFIG.hudMargin);  mixed.y =constrain(mixed.y, CONFIG.hudMargin, height -CONFIG.hudMargin);  mixed.horizontal = horizontal;  return mixed;}functiongetHudRulerLength(mark, context) {  if (mark.target ==="stemHeight") {    returnconstrain(context.plantHeight *0.86, CONFIG.hudRulerMinLength, CONFIG.hudRulerMaxLength *2.2);  }  if (mark.target ==="flowerWidth") {    returnconstrain(context.flowerSize *1.45, CONFIG.hudRulerMinLength, CONFIG.hudRulerMaxLength *1.6);  }  return mark.length;}functiondrawHudBox(mark, pulse, context) {  consttarget=getHudBoxTarget(mark, context);  constbreathe=sin((getCurrentRhythmFrame() *CONFIG.hudAnimationSpeed *0.6+ mark.phase) *360) *4;  constw= target.w + breathe;  consth= target.h + breathe *0.7;  constx=constrain(target.x - w /2+ mark.drift * pulse.scan, CONFIG.hudMargin, width -CONFIG.hudMargin - w);  consty=constrain(target.y - h /2, CONFIG.hudMargin, height -CONFIG.hudMargin - h);  constc=min(CONFIG.hudCornerLength, w *0.36, h *0.36) * pulse.reveal;  constalphaValue=CONFIG.hudAlpha * pulse.alpha;  hudLayer.stroke(255, alphaValue);  hudLayer.strokeWeight(CONFIG.hudStrokeWeight);  hudLayer.line(x, y, x + c, y);  hudLayer.line(x, y, x, y + c);  hudLayer.line(x + w, y, x + w - c, y);  hudLayer.line(x + w, y, x + w, y + c);  hudLayer.line(x, y + h, x + c, y + h);  hudLayer.line(x, y + h, x, y + h - c);  hudLayer.line(x + w, y + h, x + w - c, y + h);  hudLayer.line(x + w, y + h, x + w, y + h - c);  constscanY=lerp(y, y + h, pulse.scan);  hudLayer.stroke(255, alphaValue *0.45);  hudLayer.line(x +8, scanY, x + w -8, scanY);  hudLayer.noStroke();  hudLayer.fill(255, alphaValue);  hudLayer.textSize(14);  hudLayer.text(mark.code +" "+getHudTargetValue(mark.target), x +8, y -12);}functiongetHudBoxTarget(mark, context) {  let targetX = mark.x;  let targetY = mark.y;  let targetW = mark.w;  let targetH = mark.h;  if (mark.target ==="flower") {    targetX = context.flower.x;    targetY = context.flower.y;    targetW = context.flowerSize *1.35;    targetH = context.flowerSize *1.1;  } elseif (mark.target ==="stem") {    targetX = context.stemMid.x;    targetY = context.stemMid.y;    targetW =90;    targetH = context.plantHeight *0.42;  } elseif (mark.target ==="root") {    targetX = context.root.x;    targetY = context.root.y;    targetW =120;    targetH =70;  } elseif (mark.target ==="leaf"&& context.leaves.length>0) {    constleaf= context.leaves[floor(abs(mark.phase *77)) % context.leaves.length];    targetX = leaf.x;    targetY = leaf.y;    targetW =160;    targetH =90;  }  constmixed=mixHudPosition(mark.x, mark.y, targetX, targetY);  return {    x: mixed.x,    y: mixed.y,    w:constrain(targetW, CONFIG.hudBoxMinSize *0.65, CONFIG.hudBoxMaxSize *1.7),    h:constrain(targetH, CONFIG.hudBoxMinSize *0.45, CONFIG.hudBoxMaxSize *1.7)  };}functiondrawHudLabel(mark, pulse, context) {  constalphaValue=CONFIG.hudAlpha * pulse.alpha;  constlineLength=CONFIG.hudLabelLineLength * pulse.reveal;  consttarget=getHudLabelTarget(mark, context);  constdriftY=sin((getCurrentRhythmFrame() *CONFIG.hudAnimationSpeed + mark.phase) *360) *8;  constanchorX= target.anchor.x;  constanchorY= target.anchor.y + driftY;  constelbowX= anchorX + mark.side * lineLength;  consttextX= elbowX + mark.side *12;  constvalueText= target.text;  hudLayer.textSize(15);  consttextW= hudLayer.textWidth(valueText) +12;  constlabelX= mark.side >0? textX : textX - textW;  hudLayer.stroke(255, alphaValue);  hudLayer.strokeWeight(CONFIG.hudStrokeWeight);  hudLayer.line(anchorX, anchorY, elbowX, anchorY);  hudLayer.line(elbowX, anchorY, elbowX + mark.side *18, anchorY -16);  hudLayer.noStroke();  hudLayer.fill(255, alphaValue);  hudLayer.circle(anchorX, anchorY, 4);  hudLayer.fill(0, alphaValue *0.78);  hudLayer.rect(labelX -6, anchorY -29, textW, 22);  hudLayer.fill(255, alphaValue);  hudLayer.text(valueText, labelX, anchorY -18);}functiongetHudLabelTarget(mark, context) {  let anchor =createVector(mark.x, mark.y);  let value = mark.value;  let code = mark.code;  if (mark.target ==="growth") {    anchor = context.currentTip;    value =nf(lifeState.stemGrow, 12);    code ="GROW";  } elseif (mark.target ==="bloom") {    anchor = context.flower;    value =nf(lifeState.flowerOpen, 12);    code ="BLOOM";  } elseif (mark.target ==="wilt") {    anchor = context.stemMid;    value =nf(lifeState.wilt, 12);    code ="WILT";  } elseif (mark.target ==="pulse") {    anchor = context.energyPoint;    constpulsePosition= ((getCurrentRhythmFrame() *CONFIG.numberPulseSpeed) %1+1) %1;    value =nf(getEnergyPulseAt(pulsePosition), 12);    code ="PULSE";  } elseif (mark.target ==="height") {    anchor = context.currentTip;    value =nf(context.plantHeight /CONFIG.stemLength, 12);    code ="HEIGHT";  } elseif (mark.target ==="leaf"&& context.leaves.length>0) {    anchor = context.leaves[floor(abs(mark.phase *67)) % context.leaves.length];    value =nf(lifeState.leafGrow * (1- lifeState.leafFall), 12);    code ="LEAF";  }  constmixed=mixHudPosition(mark.x, mark.y, anchor.x, anchor.y);  mixed.x =constrain(mixed.x, CONFIG.hudMargin, width -CONFIG.hudMargin);  mixed.y =constrain(mixed.y, CONFIG.hudMargin, height -CONFIG.hudMargin);  return {    anchor: mixed,    text: code +" "+ value  };}functiongetHudTargetValue(targetName) {  if (targetName ==="flower") returnnf(lifeState.flowerOpen, 12);  if (targetName ==="stem") returnnf(lifeState.stemGrow, 12);  if (targetName ==="leaf") returnnf(lifeState.leafGrow * (1- lifeState.leafFall), 12);  if (targetName ==="root") returnnf(1- lifeState.stemAlpha, 12);  return"0.00";}functionrotatePointAroundY(point, angleDeg) {  // 当前整株植物只绕 Y 轴旋转;这里手动计算投影后的屏幕位置。  constrotatedX= point.x *cos(angleDeg) + point.z *sin(angleDeg);  constrotatedZ=-point.x *sin(angleDeg) + point.z *cos(angleDeg);  returncreateVector(rotatedX, point.y, rotatedZ);}functiongetScreenPoint(point) {  // 优先使用 p5 WEBGL 的真实投影位置,避免旋转到侧面时数字和花茎错位。  if (typeof screenPosition ==="function") {    constprojected=screenPosition(point.x, point.y, point.z);    // 不同 p5 版本返回值可能是画布坐标或 WEBGL 中心坐标,这里做一次兼容。    if (projected.x >=0&& projected.x <= width && projected.y >=0&& projected.y <= height) {      returncreateVector(projected.x, projected.y);    }    returncreateVector(projected.x + width /2, projected.y + height /2);  }  constrotated=rotatePointAroundY(point, viewRotationY + dragRotationY);  returncreateVector(rotated.x + width /2, rotated.y + height /2);}// ╔══════════════════════════════════════════════════════════╗// ║              叶片、分支与掉落动画                         ║// ╚══════════════════════════════════════════════════════════╝functiondrawLeafBranches() {  if (lifeState.leafGrow <=0.01return;  push();  strokeWeight(CONFIG.stemStrokeWeight *0.62);  noFill();  constactiveLeaves=getActiveStemLeaves();  for (let leafIndex =0; leafIndex < activeLeaves.length; leafIndex++) {    constleaf= activeLeaves[leafIndex];    constbranchAlpha=1-getLeafFallProgress(leafIndex);    conststemVisible=getStemSegmentVisible(leaf.t);    if (branchAlpha <=0.01|| stemVisible <=0.01continue;    constpulse=getEnergyPulseAt(leaf.t);    strokeWeight(CONFIG.stemStrokeWeight *0.62+ pulse *CONFIG.stemPulseWeightBoost *0.55);    stroke(getPulsedPlantGray(getStemSegmentGray(leaf.t), pulse), 255* branchAlpha * stemVisible);    beginShape();    for (let i =0; i <=CONFIG.stemSegments; i++) {      constbranchPoint=getLeafBranchPoint(leaf, (i /CONFIG.stemSegments) * lifeState.leafGrow);      vertex(branchPoint.x, branchPoint.y, branchPoint.z);    }    endShape();  }  pop();}functiondrawStemLeaves() {  if (lifeState.leafGrow <=0.01return;  push();  noStroke();  constactiveLeaves=getActiveStemLeaves();  for (let leafIndex =0; leafIndex < activeLeaves.length; leafIndex++) {    constleaf= activeLeaves[leafIndex];    if (getLeafFallProgress(leafIndex) >0.04continue;    constleafBase=getLeafBranchPoint(leaf, 1);    constbranchTangent=getLeafBranchTangent(leaf, 1);    constleafAngle=getLiftedLeafAngle(branchTangent);    constleafPulse=getEnergyPulseAt(leaf.t);    push();    // 叶片根部落在分支末端,分支根部落在主茎上。    translate(      leafBase.x -cos(leafAngle) *CONFIG.leafBaseOverlap,      leafBase.y -sin(leafAngle) *CONFIG.leafBaseOverlap,      leafBase.z +2    );    rotateZ(leafAngle);    rotateY(leaf.rotationY);    rotateX(CONFIG.leafFaceUpRotationX);    rotateZ(lifeState.wilt * leaf.side *18);    drawSinglePetalLeaf(leaf.sizeScale * lifeState.leafGrow, 255, leafPulse);    pop();  }  pop();}functiongetLeafBranchPoint(leaf, t) {  constbase=getStemPoint(leaf.t, 0);  conststemTangent=getStemTangent(leaf.t, 0);  conststemAngle=atan2(stemTangent.y, stemTangent.x);  constbranchAngle= stemAngle + leaf.side *72+ leaf.angleJitter;  constbranchLength=CONFIG.leafLength *CONFIG.leafBranchLengthRatio * leaf.sizeScale;  consteaseT= t * t * (3-2* t);  constcurve=sin(t *180) *CONFIG.stemCurveAmount *0.9* leaf.branchCurve;  constnormalAngle= branchAngle + leaf.side *90;  returncreateVector(    base.x +cos(branchAngle) * branchLength * easeT +cos(normalAngle) * curve,    base.y +sin(branchAngle) * branchLength * easeT +sin(normalAngle) * curve,    base.z + leaf.side *CONFIG.stemCurveAmount *0.45*sin(t *180)  );}functiongetLeafBranchTangent(leaf, t) {  constdelta=0.01;  constbefore=getLeafBranchPoint(leaf, max(0, t - delta));  constafter=getLeafBranchPoint(leaf, min(1, t));  return p5.Vector.sub(after, before).normalize();}functiongetLiftedLeafAngle(branchTangent) {  // p5 的屏幕 y 轴向下,因此减小 y 分量可以让叶片连接处自然向上托起。  constliftedDirection=createVector(    branchTangent.x,    branchTangent.y -CONFIG.leafConnectionLift,    branchTangent.z  ).normalize();  returnatan2(liftedDirection.y, liftedDirection.x);}functiondrawFallingLeaves() {  if (lifeState.leafFall <=0.01return;  push();  drawingContext.enable(drawingContext.BLEND);  drawingContext.blendFunc(drawingContext.SRC_ALPHA, drawingContext.ONE_MINUS_SRC_ALPHA);  constactiveLeaves=getActiveStemLeaves();  for (let leafIndex =0; leafIndex < activeLeaves.length; leafIndex++) {    constleaf= activeLeaves[leafIndex];    constfallProgress=getLeafFallProgress(leafIndex);    if (fallProgress <=0.01|| fallProgress >=1continue;    constleafBase=getLeafBranchPoint(leaf, 1);    constbranchTangent=getLeafBranchTangent(leaf, 1);    constleafAngle=getLiftedLeafAngle(branchTangent);    constdriftSeed=sin((leafIndex +1) *117.3);    constdrop=CONFIG.fallingLeafDistance * fallProgress * fallProgress;    constdrift=CONFIG.fallingLeafDrift * driftSeed *smoothStep(01, fallProgress);    constalphaValue=255* (1-smoothStep(0.621, fallProgress));    constleafPulse=getEnergyPulseAt(leaf.t);    push();    translate(      leafBase.x -cos(leafAngle) *CONFIG.leafBaseOverlap + drift,      leafBase.y -sin(leafAngle) *CONFIG.leafBaseOverlap + drop,      leafBase.z +18*sin(fallProgress *180)    );    rotateZ(leafAngle + fallProgress *95* leaf.side);    rotateY(leaf.rotationY + fallProgress *120* driftSeed);    rotateX(CONFIG.leafFaceUpRotationX + fallProgress *85);    drawSinglePetalLeaf(leaf.sizeScale * lifeState.leafGrow, alphaValue, leafPulse);    pop();  }  pop();}functiongetLeafFallProgress(leafIndex) {  constcount=max(1, getActiveStemLeaves().length);  conststagger= count ===1?0: leafIndex / (count -1) *0.16;  conststart=CONFIG.leafFallStart + stagger;  constend=CONFIG.leafFallEnd + stagger;  returnsmoothStep(start, end, lifeState.wilt);}functiondrawSinglePetalLeaf(sizeScale, alphaValue=255, pulse=0) {  constlength=CONFIG.leafLength * sizeScale;  constwidth=CONFIG.leafWidth * sizeScale;  for (let row =0; row <CONFIG.leafRows; row++) {    constu0= row /CONFIG.leafRows;    constu1= (row +1) /CONFIG.leafRows;    constgrayValue=getPulsedPlantGray(getHighContrastPlantGray(      map(row, 0, CONFIG.leafRows -1, CONFIG.grayMax, CONFIG.grayMin)    ), pulse);    fill(grayValue, alphaValue);    ambientMaterial(grayValue);    beginShape(QUAD_STRIP);    for (let col =0; col <=CONFIG.leafCols; col++) {      constv=map(col, 0, CONFIG.leafCols, -11);      constp0=getSinglePetalLeafPoint(u0, v, length, width);      constp1=getSinglePetalLeafPoint(u1, v, length, width);      vertex(p0.x, p0.y, p0.z);      vertex(p1.x, p1.y, p1.z);    }    endShape();  }}functiongetSinglePetalLeafPoint(u, v, length, width) {  // 这里复用花朵的参数化思路:长度方向 + 宽度方向形成一个弯曲曲面。  constprofile=pow(sin(u *180), CONFIG.leafSharpness);  consttaper=1-pow(u, 1.8) *0.15;  constx= u * length;  consty= v * width * profile * taper;  constarch=sin(u *180) *CONFIG.leafArchHeight;  constcurl= v * v *CONFIG.leafCurl *sin(u *180);  constz= arch - curl;  returncreateVector(x, y, z);}// ╔══════════════════════════════════════════════════════════╗// ║              花朵、花托与花瓣凋落                         ║// ╚══════════════════════════════════════════════════════════╝functiondrawFlowerAtStemTop() {  if (lifeState.flowerOpen <=0.01return;  constattachmentT=getFlowerAttachmentT();  conststemTop=getStemPoint(attachmentT, 0);  conststemTangent=getStemTangent(attachmentT, 0);  consttangentAngle=atan2(stemTangent.y, stemTangent.x);  constuprightAngle=-90;  constplant= currentPlant || {};  constfollowAngle= (tangentAngle - uprightAngle) * (plant.flowerFollowStrength ??CONFIG.flowerFollowStemDirection);  push();  translate(stemTop.x, stemTop.y, stemTop.z);  // 让花朵朝向受到花茎顶端切线影响;底端仍固定在花茎顶点。  drawFlowerNeck(stemTangent);  rotateZ(followAngle);  rotateZ(getWindAt(10) *CONFIG.flowerWindNod);  rotateY(getWindAt(0.720) *CONFIG.flowerWindSway);  rotateZ(lifeState.wilt *CONFIG.wiltDroopAngle);  push();  rotateX(plant.flowerTiltX ??CONFIG.tiltX);  rotateY(plant.flowerTiltY ??0);  rotateZ((plant.flowerTiltZ ??CONFIG.tiltZ) + (plant.flowerSpin ??0));  constflowerSize= (plant.flowerScale ??CONFIG.flowerScale) * lifeState.flowerOpen;  scale(    flowerSize * (plant.flowerStretchX ??1),    flowerSize * (plant.flowerStretchY ??1),    flowerSize * (plant.flowerStretchZ ??1)  );  // 先把花朵模型底端移动到局部原点,再绘制到花茎顶端。  translate(00, -CONFIG.flowerBottomLocalZ);  noStroke();  constflowerGray=getHighContrastPlantGray(    constrain(lerp(CONFIG.grayMax, CONFIG.grayMin, lifeState.wilt *0.75) + (plant.grayShift ??0), 0255)  );  fill(flowerGray);  ambientMaterial(flowerGray);  if (lifeState.wilt >0.01) {    drawRemainingFlowerSurface(flowerGray);  } else {    model(flowerGeometry);  }  pop();  drawFallingPetals();  pop();}functiondrawFlowerNeck(stemTangent) {  // 花托用于柔化花茎顶端和花朵底部的连接,方向取自茎顶切线,末端轻轻向花头中心收住。  constalphaValue=255* lifeState.flowerOpen * lifeState.stemAlpha;  if (alphaValue <=2return;  constneckLength=CONFIG.flowerNeckLength * lifeState.flowerOpen;  consttangent= stemTangent.copy().normalize();  push();  noFill();  stroke(CONFIG.stemColor, alphaValue);  strokeWeight(CONFIG.flowerNeckStrokeWeight);  beginShape();  vertex(-tangent.x * neckLength *0.55, -tangent.y * neckLength *0.55, -tangent.z * neckLength *0.55);  vertex(-tangent.x * neckLength *0.25, -tangent.y * neckLength *0.25, -tangent.z * neckLength *0.25);  vertex(000);  endShape();  pop();}functiondrawRemainingFlowerSurface(baseGray) {  fill(baseGray);  ambientMaterial(baseGray);  for (let i =0; i < flowerPetalGeometries.length; i++) {    if (getPetalFallProgress(i) >0.08continue;    model(flowerPetalGeometries[i]);  }}functiondrawFallingPetals() {  if (lifeState.wilt <=0.01return;  push();  drawingContext.enable(drawingContext.BLEND);  drawingContext.blendFunc(drawingContext.SRC_ALPHA, drawingContext.ONE_MINUS_SRC_ALPHA);  constpetalTotal=floor(CONFIG.petals);  constpetalWidth=CONFIG.petalLength *0.95;  constpetalLength=CONFIG.diameter +CONFIG.petalLength *1.35;  for (let i =0; i < petalTotal; i++) {    constfallProgress=getPetalFallProgress(i);    if (fallProgress <=0.01|| fallProgress >=1continue;    constangle=360* i / petalTotal;    constdriftSeed=sin(i *91.7);    constsideSeed=cos(i *53.3);    constgravityProgress= fallProgress * fallProgress;    constdrop=CONFIG.fallingPetalDistance * (0.16* fallProgress +0.84* gravityProgress);    constdrift=CONFIG.fallingPetalDrift * driftSeed *smoothStep(01, fallProgress);    constoutX=cos(angle) * (CONFIG.diameter +CONFIG.petalLength *0.45);    constoutZ=sin(angle) * (CONFIG.diameter +CONFIG.petalLength *0.45);    constspin=CONFIG.fallingPetalSpin * fallProgress * (i %2===0?1:-1);    constpetalPulse=getEnergyPulseAt(1);    constgrayValue=getPulsedPlantGray(getHighContrastPlantGray(      lerp(CONFIG.grayMax, CONFIG.grayMin, fallProgress *0.85)    ), petalPulse);    constalphaValue=255* (1-smoothStep(CONFIG.fallingPetalFadeStart, 1, fallProgress));    push();    translate(outX + drift, drop, outZ + sideSeed *34);    rotateZ(angle *0.35+ spin);    rotateY(sideSeed *40+ spin *0.25);    rotateX(70+55* fallProgress);    scale(CONFIG.fallingPetalScale);    fill(grayValue, alphaValue);    ambientMaterial(grayValue);    drawDetachedPetalSurface(petalLength, petalWidth, fallProgress);    pop();  }  pop();}functiongetPetalFallProgress(petalIndex) {  constpetalTotal=floor(CONFIG.petals);  conststart= petalIndex / petalTotal * (1-CONFIG.fallingPetalDuration);  constend= start +CONFIG.fallingPetalDuration;  returnsmoothStep(start, end, lifeState.wilt);}functiongetPetalIndexFromAngle(angle) {  constnormalized= ((angle %360) +360) %360;  returnfloor(normalized /360*floor(CONFIG.petals)) %floor(CONFIG.petals);}functiondrawDetachedPetalSurface(petalLength, petalWidth, fallProgress) {  for (let row =0; row <CONFIG.fallingPetalRows; row++) {    constu0= row /CONFIG.fallingPetalRows;    constu1= (row +1) /CONFIG.fallingPetalRows;    beginShape(QUAD_STRIP);    for (let col =0; col <=CONFIG.fallingPetalCols; col++) {      constv=map(col, 0, CONFIG.fallingPetalCols, -11);      constp0=getDetachedPetalPoint(u0, v, petalLength, petalWidth, fallProgress);      constp1=getDetachedPetalPoint(u1, v, petalLength, petalWidth, fallProgress);      vertex(p0.x, p0.y, p0.z);      vertex(p1.x, p1.y, p1.z);    }    endShape();  }}functiongetDetachedPetalPoint(u, v, petalLength, petalWidth, fallProgress) {  constprofile=pow(sin(u *180), CONFIG.leafSharpness);  constx= u * petalLength;  consty= v * petalWidth * profile;  constz=    sin(u *180) *CONFIG.leafArchHeight *0.7-    v * v *CONFIG.leafCurl *0.7*sin(u *180) -    fallProgress *18;  returncreateVector(x, y, z);}// ╔══════════════════════════════════════════════════════════╗// ║              三维茎线、风摆与基础曲面公式                 ║// ╚══════════════════════════════════════════════════════════╝functiongetStemPoint(t, stemIndex) {  constplant= currentPlant || {};  constratio=CONFIG.stemCount ===1?0.5: stemIndex / (CONFIG.stemCount -1);  constrootX= plant.rootX ??0;  constrootY= plant.rootY ?? (CONFIG.stemTopY +CONFIG.stemLength +CONFIG.commonRootOutsideOffset);  constrootZ= plant.rootZ ??0;  consttopX= (plant.stemTopX ??CONFIG.stemTopX) +map(ratio, 01, -CONFIG.stemSpread, CONFIG.stemSpread);  consttopY= plant.stemTopY ??CONFIG.stemTopY;  consttopZ= plant.stemTopZ ??CONFIG.stemTopZ;  consteaseT= t * t * (3-2* t);  // 使用两组正弦弯曲,形成有生命感的弧形花茎。  constwiltBend=smoothStep(0.12, CONFIG.stemCollapseStart, lifeState.wilt);  constextraWiltCurve=sin(t *180) *CONFIG.stemWiltExtraBend * wiltBend;  constmainCurve=sin(t *180) * (plant.stemMainBend ??CONFIG.stemMainBend) + extraWiltCurve;  constsecondaryCurve=sin(t *360) * (plant.stemSecondaryBend ??CONFIG.stemSecondaryBend);  constside= ratio -0.5;  constwind=getWindAt(t, stemIndex) *CONFIG.windAmount *pow(t, 1.7) * lifeState.stemGrow;  constcollapse= lifeState.stemCollapse;  constcollapsePower=pow(t, 1.45);  constcollapseX=CONFIG.stemCollapseBend * collapse * collapsePower;  constcollapseY=CONFIG.stemCollapseDrop * collapse * collapsePower;  returncreateVector(    lerp(rootX, topX, easeT) + mainCurve + secondaryCurve + side * (plant.stemCurveAmount ??CONFIG.stemCurveAmount) + wind + collapseX,    lerp(rootY, topY, easeT) + collapseY,    lerp(rootZ, topZ, easeT)  );}functiongetWindAt(t, stemIndex) {  // 底部基本不动,顶部随风轻摆;相位沿高度变化,避免整根茎像刚体一样摆。  constplant= currentPlant || {};  constlocalFrame=getCurrentRhythmFrame();  returnsin(localFrame *CONFIG.windSpeed *360+ t *180*CONFIG.windWave + stemIndex *43+ (plant.windPhase ??0));}functiongetStemTangent(t, stemIndex) {  constdelta=0.01;  constbefore=getStemPoint(max(0, t - delta), stemIndex);  constafter=getStemPoint(min(1, t), stemIndex);  return p5.Vector.sub(after, before).normalize();}functionvShape(height, radius, curveA, curveB, curveC) {  return height *pow(Math.E, -curveB *pow(abs(radius), curveC)) *pow(abs(radius), curveA);}functionsurfaceBump(amount, radius, frequency, angle) {  return1+ amount *pow(radius, 2) *sin(frequency * angle);}