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:
共享根簇 / Shared root cluster 两朵花从画布下方相近但略微错开的起点生长,形成同源但不完全重合的根部关系。 Two flowers grow from nearby, slightly offset origins below the canvas, creating a shared but not identical root system. 线性花茎 / Linear stems 花茎由分段三维曲线构成,包含自然弯曲、风摆、能量脉冲、倒伏和分段消失。 The stems are segmented 3D curves with natural bending, wind sway, energy pulses, collapse, and top-down dissolution. 程序化花朵 / 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. 数字花托 / Numeric calyx 茎顶和花底之间叠加密集的“数字 + 黑框”,模拟花托结构,也让花与茎的衔接更自然。 Dense black-box numeric labels around the stem tip mimic a calyx and visually bridge the stem-flower connection. 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
randomizeOnRun 为 true 时,生成新的花丛 / Generates a new flower cluster when randomizeOnRun is true |
技术实现 / Technical Details
文件结构 / 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.
canvasWidthcanvasHeight | |
flowerScale | |
flowerCount | |
randomizeOnRun | |
flowerClusterSeed | |
flowerRootSpreadXflowerRootSpreadZ | |
flowerClusterHeightStagger | |
rhythmBeatFrames | |
rhythmLifecycleBeats | |
rhythmPulseCyclesPerBeat | |
bloomStartStemGrowRatio | |
calyxNumberCount | |
stemLengthstemMainBend | |
leafLengthleafWidth | |
numberPulseSpeed | |
hudAlphahudStrokeWeight | |
hudFadeOutStart | |
hudRulerCounthudBoxCount / hudLabelCount |
创作 / Credits
"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 表示刷新页面会得到新的花丛、叶片、数字和 HUDrandomizeOnRun: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 表示脉冲峰值可把数值推到 1numberPulseValueBoost:0.85,// 全株能量脉冲对花、叶、茎的亮度推动强度plantPulseBrightness:115,// 全株能量脉冲经过茎时额外增加的线宽stemPulseWeightBoost:5,// 凋零时数字开始从上往下归零的凋零进度numberDrainStart:0.42,// 凋零时数字全部归零消失的凋零进度numberDrainEnd:0.9,// 单个数字归零的过渡宽度,越小越像一格一格熄灭numberDrainStepWidth:0.08,// ╔══════════════════════════════════════════════════════════╗// ║ 生长、开花、凋零生命周期参数 ║// ╚══════════════════════════════════════════════════════════╝// 生命周期总时长,单位为帧;节奏同步开启时由 rhythmBeatFrames 和 rhythmLifecycleBeats 自动计算lifecycleFrames:720,// 幼苗到主茎基本长成的时间点,0 到 1sproutEnd:0.34,// 花朵开始成长时对应的茎生长比例;0.33 表示茎长到三分之一时花就开始成长bloomStartStemGrowRatio:0.33,// 花朵从花苞到盛开的时间点,0 到 1bloomEnd:0.58,// 开始凋零的时间点,0 到 1wiltStart: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,// 茎开始分段变黑消失的凋零进度;应晚于 stemCollapseStartstemFadeStart: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), 1, 5);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(-14, 14),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(-46, 46),stemTopY:CONFIG.stemTopY + heightPattern *CONFIG.flowerClusterHeightStagger +random(-CONFIG.flowerClusterTopYJitter, CONFIG.flowerClusterTopYJitter),stemTopZ:CONFIG.stemTopZ +random(-CONFIG.flowerClusterSpreadZ, CONFIG.flowerClusterSpreadZ),stemMainBend:CONFIG.stemMainBend *random(0.65, 1.2) + centered *90,stemSecondaryBend:CONFIG.stemSecondaryBend *random(0.65, 1.35),stemCurveAmount:CONFIG.stemCurveAmount *random(0.7, 1.5),flowerScale:CONFIG.flowerScale *random(0.62, 1.18),flowerStretchX:random(0.72, 1.32),flowerStretchY:random(0.76, 1.28),flowerStretchZ:random(0.84, 1.2),flowerTiltX:CONFIG.tiltX +random(-CONFIG.flowerClusterPitchJitter, CONFIG.flowerClusterPitchJitter),flowerTiltY: centered *CONFIG.flowerClusterYawSpread +random(-4, 4),flowerTiltZ:CONFIG.tiltZ + centered *72+random(-36, 36),flowerSpin:random(0, 360),flowerFollowStrength:CONFIG.flowerFollowStemDirection *random(0.78, 1.08),grayShift:random(-34, 34),windPhase:random(0, 360),leafSeedOffset,numberSeedOffset};plant.leaves =createStemLeaves(leafSeedOffset);plant.numbers =createStemNumbers(plant.leaves, numberSeedOffset);flowerPlants.push(plant);}}functiongetFlowerClusterHeightPattern(index, total) {// 参考图中的花丛不是平均排布,而是高低穿插:高枝、中过渡、低枝同时存在。constpatterns= {1: [0],2: [-0.42, 0.34],3: [-0.48, -0.08, 0.42],4: [-0.5, -0.18, 0.24, 0.48],5: [-0.52, -0.18, -0.38, 0.46, 0.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), 0, 1);return t * t * (3-2* t);}functiongetHighContrastPlantGray(grayValue) {// 只用于花朵和叶片:把明暗范围压到极致,暗部为黑,亮部为白。constnormalized=constrain(grayValue /255, 0, 1);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.01) return;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.01) continue;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(0, 0);push();translate(rootPoint.x, rootPoint.y, rootPoint.z);noStroke();fill(CONFIG.stemColor);circle(0, 0, 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.32, 0.72),side: i %2===0?-1:1,sizeScale:random(0.86, 1.15),angleJitter:random(-12, 12),rotationY:random(CONFIG.leafRandomRotationYMin, CONFIG.leafRandomRotationYMax),branchCurve:random(-1, 1),zLift:random(8, 26)});}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.02, 1);}functionrandomStemNumber() {returnnf(random(0.01, 0.99), 1, 2);}// ╔══════════════════════════════════════════════════════════╗// ║ 数字标签、花托数字与能量脉冲 ║// ╚══════════════════════════════════════════════════════════╝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.72, 1, dissolve);}functiongetStemSegmentGray(t) {constdissolve=getTopDownDissolveProgress(t,CONFIG.stemFadeStart,CONFIG.stemFadeEnd,CONFIG.stemSegmentFadeWidth);returnlerp(CONFIG.stemColor, 0, smoothStep(0, 0.72, dissolve));}functiondrawStemNumbers() {if (lifeState.numberReveal <=0.01|| lifeState.stemAlpha <=0.01) return;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 >=1) continue;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.01) return;constattachmentT=getFlowerAttachmentT();conststemTop=getStemPoint(attachmentT, 0);constplantIndex= currentPlant ? currentPlant.index :0;constappear=smoothStep(0.06, 0.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), 1, 2),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, 0, 1);// 平滑的单峰脉冲:能量经过时放大,离开后自然回落。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.01, 1);constboostedValue= baseValue + (1- baseValue) * pulse *CONFIG.numberPulseValueBoost;constdrainedValue=lerp(min(1, boostedValue), 0, drain);// 生长时数值随脉冲向 1 靠近;凋零时从上到下依次减少到 0。returnnf(constrain(drainedValue, 0, 1), 1, 2);}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(255, 185+ pulse *CONFIG.numberPulseBrightness);numberLayer.noStroke();numberLayer.fill(0, 230* 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(255, 205+ 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(5, 13)),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(-360, 360),y: height /2+random(-620, 560),w: boxSize *random(0.72, 1.35),h: boxSize *random(0.58, 1.08),phase:random(),drift:random(-10, 10),code:"SEL-"+nf(i +1, 2, 0),target:random(["flower", "stem", "leaf", "root"])});}for (let i =0; i <CONFIG.hudLabelCount; i++) {constside=random() >0.5?1:-1;constvalue=random(0.01, 0.99);hudMarks.push({kind:"label",x: width /2+ side *random(250, 520),y:random(CONFIG.hudMargin +80, height -CONFIG.hudMargin -140),side,phase:random(),value:nf(value, 1, 2),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.01) return;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.01) continue;constbasePulse=getHudPulse(mark.phase);constpulse= {phase: basePulse.phase,alpha: basePulse.alpha * visibility,reveal: basePulse.reveal,scan: basePulse.scan};if (pulse.alpha <=0.01) continue;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.05, 0.48, phase);constscan=smoothStep(0.18, 0.86, phase);return { phase, alpha, reveal, scan };}functiongetHudContext() {constroot=projectPlantPoint(getStemPoint(0, 0));constgrowT=constrain(lifeState.stemGrow, 0.001, 1);constflowerT=getFlowerAttachmentT();constcurrentTip=projectPlantPoint(getStemPoint(growT, 0));conststemMid=projectPlantPoint(getStemPoint(growT *0.55, 0));constflower=projectPlantPoint(getStemPoint(flowerT, 0));constflowerSize=lerp(70, 230, 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.96) continue;leaves.push(projectPlantPoint(getLeafBranchPoint(leaf, lifeState.leafGrow)));}return {root,currentTip,stemMid,flower,flowerSize,plantHeight,energyPoint,leaves,stemVisible:smoothStep(0.02, 0.16, lifeState.stemGrow) * lifeState.stemAlpha,flowerVisible:smoothStep(0.03, 0.22, lifeState.flowerOpen) * (1-smoothStep(0.82, 1, lifeState.wilt)),leafVisible:smoothStep(0.03, 0.2, lifeState.leafGrow) * (1- lifeState.leafFall),pulseVisible:smoothStep(0.12, 0.36, lifeState.stemGrow) * (1-smoothStep(0, 0.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.02, 0.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, 1, 2);code ="GROW";} elseif (mark.target ==="bloom") {anchor = context.flower;value =nf(lifeState.flowerOpen, 1, 2);code ="BLOOM";} elseif (mark.target ==="wilt") {anchor = context.stemMid;value =nf(lifeState.wilt, 1, 2);code ="WILT";} elseif (mark.target ==="pulse") {anchor = context.energyPoint;constpulsePosition= ((getCurrentRhythmFrame() *CONFIG.numberPulseSpeed) %1+1) %1;value =nf(getEnergyPulseAt(pulsePosition), 1, 2);code ="PULSE";} elseif (mark.target ==="height") {anchor = context.currentTip;value =nf(context.plantHeight /CONFIG.stemLength, 1, 2);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), 1, 2);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, 1, 2);if (targetName ==="stem") returnnf(lifeState.stemGrow, 1, 2);if (targetName ==="leaf") returnnf(lifeState.leafGrow * (1- lifeState.leafFall), 1, 2);if (targetName ==="root") returnnf(1- lifeState.stemAlpha, 1, 2);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.01) return;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.01) continue;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.01) return;push();noStroke();constactiveLeaves=getActiveStemLeaves();for (let leafIndex =0; leafIndex < activeLeaves.length; leafIndex++) {constleaf= activeLeaves[leafIndex];if (getLeafFallProgress(leafIndex) >0.04) continue;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.01) return;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 >=1) continue;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(0, 1, fallProgress);constalphaValue=255* (1-smoothStep(0.62, 1, 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, -1, 1);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.01) return;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(1, 0) *CONFIG.flowerWindNod);rotateY(getWindAt(0.72, 0) *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(0, 0, -CONFIG.flowerBottomLocalZ);noStroke();constflowerGray=getHighContrastPlantGray(constrain(lerp(CONFIG.grayMax, CONFIG.grayMin, lifeState.wilt *0.75) + (plant.grayShift ??0), 0, 255));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 <=2) return;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(0, 0, 0);endShape();pop();}functiondrawRemainingFlowerSurface(baseGray) {fill(baseGray);ambientMaterial(baseGray);for (let i =0; i < flowerPetalGeometries.length; i++) {if (getPetalFallProgress(i) >0.08) continue;model(flowerPetalGeometries[i]);}}functiondrawFallingPetals() {if (lifeState.wilt <=0.01) return;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 >=1) continue;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(0, 1, 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, -1, 1);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, 0, 1, -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);}
夜雨聆风