[READ-ONLY] Mirror of https://github.com/flo-bit/shapecraft. flo-bit.dev/shapecraft/
0

Configure Feed

Select the types of activity you want to include in your feed.

shapecraft / src / generators / palm-tree.ts
8.9 kB 218 lines
1import { sphere } from '../primitives' 2import { merge, tube, loft, thicken } from '../ops' 3import { createRng } from '../core/rng' 4import { resolveOptions } from '../core/schema' 5import { paletteGradient, pickRandom } from '../color' 6import { UberNoise } from '../noise' 7import type { Mesh } from '../core/mesh' 8import type { Vec3 } from '../core/types' 9import type { OptionSchema } from '../core/schema' 10 11export const palmSchema = { 12 seed: { type: 'integer', default: 1, min: 1, max: 100, label: 'Seed' }, 13 height: { type: 'range', default: 3.5, min: 1.5, max: 7, step: 0.1, label: 'Height' }, 14 trunkRadius: { type: 'range', default: 0.07, min: 0.03, max: 0.2, step: 0.01, label: 'Trunk Radius' }, 15 trunkTaper: { type: 'range', default: 0.65, min: 0.1, max: 0.9, step: 0.05, label: 'Trunk Taper' }, 16 trunkCurve: { type: 'range', default: [0.3, 0.8], min: 0, max: 1.5, step: 0.05, label: 'Trunk Curve' }, 17 trunkSegments: { type: 'integer', default: 5, min: 4, max: 8, label: 'Trunk Sides' }, 18 trunkPathPoints: { type: 'integer', default: 20, min: 4, max: 40, label: 'Trunk Smoothness' }, 19 fronds: { type: 'integer', default: [6, 9], min: 3, max: 14, label: 'Fronds' }, 20 frondLength: { type: 'range', default: 1.2, min: 0.4, max: 2.5, step: 0.1, label: 'Frond Length' }, 21 frondDroop: { type: 'range', default: [0.9, 1.4], min: 0.1, max: 2, step: 0.05, label: 'Frond Droop' }, 22 frondWidth: { type: 'range', default: [0.15, 0.35], min: 0.05, max: 0.6, step: 0.02, label: 'Frond Width' }, 23 frondThickness: { type: 'range', default: 0.015, min: 0.005, max: 0.05, step: 0.005, label: 'Frond Thickness' }, 24 frondSegments: { type: 'integer', default: 5, min: 3, max: 10, label: 'Frond Segments' }, 25 frondCurveUp: { type: 'range', default: 0.3, min: 0, max: 0.8, step: 0.05, label: 'Frond Curve Up' }, 26 coconuts: { type: 'integer', default: [0, 4], min: 0, max: 6, label: 'Coconuts' }, 27 coconutSize: { type: 'range', default: [0.05, 0.09], min: 0.02, max: 0.12, step: 0.005, label: 'Coconut Size' }, 28 jitter: { type: 'range', default: 0.02, min: 0, max: 0.08, step: 0.005, label: 'Jitter' }, 29 snowColors: { type: 'color-array', default: [], min: 0, max: 6, label: 'Snow Colors' }, 30 snowAngle: { type: 'range', default: 20, min: 0, max: 80, step: 5, label: 'Snow Min Angle (°)' }, 31 trunkColors: { type: 'color-array', default: ['#3a2a15', '#5a4025', '#6a5030'], min: 2, max: 6, label: 'Trunk Colors' }, 32 frondColors: { type: 'color-array', default: ['#1a4a12', '#224e18', '#2a5520', '#1e4015'], min: 1, max: 8, label: 'Frond Colors' }, 33 coconutColor: { type: 'color', default: '#3a2810', label: 'Coconut Color' }, 34} satisfies OptionSchema 35 36export type PalmOptions = { 37 [K in keyof typeof palmSchema]?: typeof palmSchema[K]['default'] 38} & { preset?: string } 39 40export const palmPresets: Record<string, Partial<PalmOptions>> = { 41 default: {}, 42 tall: { 43 height: 6, 44 trunkCurve: [0.1, 0.4], 45 frondLength: 1.8, 46 fronds: [8, 12], 47 }, 48 short: { 49 height: 2, 50 trunkCurve: [0.5, 1], 51 frondLength: 0.8, 52 fronds: [5, 7], 53 }, 54 winter: { 55 frondColors: ['#1a4a15', '#154012'], 56 snowColors: ['#e8e8f0', '#dddde8', '#f0f0f5'], 57 snowAngle: 10, 58 trunkColors: ['#2a2018', '#3a2a1a', '#4a3a2a'], 59 }, 60} 61 62export function palm(options: PalmOptions = {}): Mesh { 63 const seed = options.seed ?? palmSchema.seed.default 64 const rand = createRng(seed) 65 const o = resolveOptions(palmSchema, options, palmPresets, rand) 66 67 function subSeed() { return Math.floor(rand() * 2147483647) } 68 69 // --- Trunk path: curved from base to top --- 70 const trunkHeight = o.height * 0.75 71 const baseRadius = o.trunkRadius * (1.3 + rand() * 0.4) 72 const curveAngle = rand() * Math.PI * 2 73 const curveAmount = o.trunkCurve 74 const curveDirX = Math.cos(curveAngle) 75 const curveDirZ = Math.sin(curveAngle) 76 77 const trunkPath: Vec3[] = [] 78 for (let i = 0; i <= o.trunkPathPoints; i++) { 79 const t = i / o.trunkPathPoints 80 const y = t * trunkHeight 81 // S-curve lean 82 const offset = Math.sin(t * Math.PI * 0.8) * t * curveAmount 83 trunkPath.push([ 84 curveDirX * offset, 85 y, 86 curveDirZ * offset, 87 ]) 88 } 89 90 // Trunk top position 91 const topPt = trunkPath[trunkPath.length - 1] 92 93 const trunkMesh = tube( 94 trunkPath, 95 (t) => { 96 const taper = baseRadius * (1 - t * o.trunkTaper) 97 const wave = 1 + Math.sin(t * Math.PI * 14) * 0.15 98 return taper * wave 99 }, 100 o.trunkSegments, 101 ) 102 .jitter(baseRadius * 0.1, { seed: subSeed() }) 103 .vertexColor((pos) => { 104 const t = Math.max(0, Math.min(1, pos[1] / trunkHeight)) 105 return paletteGradient(o.trunkColors)(t) 106 }) 107 108 // --- Fronds: flat planes thickened, arcing outward and drooping --- 109 const frondParts: Mesh[] = [] 110 const frondCount = o.fronds 111 const frondGrad = paletteGradient(o.frondColors) 112 113 // Pre-allocate seeds 114 const maxFronds = 14 115 const frondSeeds = Array.from({ length: maxFronds }, () => subSeed()) 116 const colorNoiseSeed = subSeed() 117 const snowNoiseSeed = subSeed() 118 119 const colorNoise = new UberNoise({ seed: colorNoiseSeed, scale: 1.5 }) 120 const hasSnow = o.snowColors.length > 0 121 const snowNoise = hasSnow ? new UberNoise({ seed: snowNoiseSeed, scale: 2 }) : null 122 const snowThreshold = Math.sin(o.snowAngle * Math.PI / 180) 123 124 for (let i = 0; i < frondCount; i++) { 125 const angle = (i / frondCount) * Math.PI * 2 + rand() * 0.5 126 const frondLen = o.frondLength * (0.5 + rand() * 0.7) 127 const droop = o.frondDroop * (0.3 + rand() * 1) 128 const curveUp = o.frondCurveUp * (0.5 + rand() * 1.5) 129 const width = o.frondWidth * (0.6 + rand() * 0.8) 130 const startAngleUp = rand() * 0.6 // some fronds point more upward 131 132 // Build frond path: starts at trunk top, arcs out and droops 133 const frondPath: Vec3[] = [] 134 const segs = o.frondSegments 135 const dirX = Math.cos(angle) 136 const dirZ = Math.sin(angle) 137 138 for (let j = 0; j <= segs; j++) { 139 const t = j / segs 140 const outward = t * frondLen 141 // Initial upward angle + arc curve, then gravity droop 142 const upStart = t * startAngleUp * frondLen 143 const upCurve = Math.sin(t * Math.PI * 0.3) * curveUp * frondLen 144 const droopCurve = t * t * droop * frondLen 145 frondPath.push([ 146 topPt[0] + dirX * outward, 147 topPt[1] + upStart + upCurve - droopCurve, 148 topPt[2] + dirZ * outward, 149 ]) 150 } 151 152 // Frond: loft an open U-shape that tapers toward the tip 153 const frondLoft = loft({ 154 path: frondPath, 155 closedShape: false, 156 closed: false, 157 up: [0, 1, 0], 158 shape: (t) => { 159 // Narrow at root, widens, then tapers to tip 160 const envelope = Math.sin(t * Math.PI) * (1 - t * 0.3) 161 const w = width * Math.max(0.05, envelope) 162 const h = w * 0.3 163 // Reversed order so normals face up 164 return [ 165 [w, -h * 0.3], 166 [w * 0.7, h * 0.3], 167 [w * 0.3, h * 0.7], 168 [0, h], 169 [-w * 0.3, h * 0.7], 170 [-w * 0.7, h * 0.3], 171 [-w, -h * 0.3], 172 ] 173 }, 174 }) 175 const frondMesh = thicken(frondLoft, o.frondThickness) 176 .jitter(width * o.jitter, { seed: frondSeeds[i] }) 177 178 // Color 179 const base = frondGrad(i / frondCount) 180 const snow = pickRandom(hasSnow ? o.snowColors : o.frondColors, rand) 181 rand() // consume for stability 182 183 const colored = frondMesh.faceColor((centroid, normal) => { 184 if (hasSnow && snowNoise) { 185 const n = snowNoise.get(centroid[0], centroid[1], centroid[2]) * 0.15 186 if (normal[1] + n > snowThreshold) return snow 187 } 188 const top = normal[1] * 0.5 + 0.5 189 const n = colorNoise.get(centroid[0], centroid[1], centroid[2]) * 0.1 190 const darken = 0.6 + top * 0.4 + n 191 return [base[0] * darken, base[1] * darken, base[2] * darken] 192 }) 193 194 frondParts.push(colored) 195 } 196 197 // --- Coconuts --- 198 const coconutParts: Mesh[] = [] 199 const coconutCount = o.coconuts 200 for (let i = 0; i < coconutCount; i++) { 201 const angle = rand() * Math.PI * 2 202 const topRadius = baseRadius * (1 - o.trunkTaper) 203 const dist = topRadius * (1.5 + rand() * 1) 204 const size = o.coconutSize * (0.7 + rand() * 0.6) 205 const hangY = size * (0.3 + rand() * 0.8) 206 const coconut = sphere({ radius: size, widthSegments: 4, heightSegments: 3 }) 207 .scale(0.9 + rand() * 0.2, 1 + rand() * 0.3, 0.9 + rand() * 0.2) 208 .translate( 209 topPt[0] + Math.cos(angle) * dist, 210 topPt[1] - hangY, 211 topPt[2] + Math.sin(angle) * dist, 212 ) 213 .vertexColor(o.coconutColor) 214 coconutParts.push(coconut) 215 } 216 217 return merge(trunkMesh, ...frondParts, ...coconutParts) 218}