Skip to content

Instantly share code, notes, and snippets.

@youngrichu
Created June 24, 2025 15:47
Show Gist options
  • Save youngrichu/146d1e5af20b1f85c0d52d6dae337e4c to your computer and use it in GitHub Desktop.
Save youngrichu/146d1e5af20b1f85c0d52d6dae337e4c to your computer and use it in GitHub Desktop.
Flowery ๐ŸŒธ

Flowery ๐ŸŒธ

SVG generative flowery plant. Click anywhere to generate a new plant with new colors. Then hit download to save forever. After that you'll probably want to print it and replace all your family portraits throughout your home.

Code is also in GitHub https://github.com/ste-vg/plant-drawer

A Pen by Steve Gardner on CodePen.

License.

<div id="app">
<svg id="stage" width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<g id="branchGroup"></g>
<g id="thornGroup"></g>
<g id="leafGroup"></g>
<g id="flowerGroup"></g>
</svg>
</div>
<a id="download-button">Download</a>
<a href="https://github.com/ste-vg/plant-drawer" target="_blank" class="github-corner" aria-label="View source on Github"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style>
// Tidier code with webpack and better Typescript in Github
// https://github.com/ste-vg/plant-drawer
console.clear();
interface Position
{
x: number;
y: number;
}
enum BranchState
{
ready,
animating,
ended
}
interface BranchSettings
{
x: number;
y: number;
directionX?: number;
directionY?: number;
length?: number;
sections: number;
width?: number;
chunkLength?: number;
color?: string;
progress?: number;
opacity?: number;
}
interface FlowerColors
{
outer: string;
inner: string;
}
interface BranchSet
{
path: SVGPathElement;
settings: BranchSettings;
}
interface Out
{
position: Position;
width?: number;
sections?: number;
}
class Branch
{
private grid:number;
private stage:HTMLElement;
private branch:SVGPathElement;
public branches: BranchSet[] = [];
private settings:BranchSettings;
public state:BranchState = BranchState.ready;
private placeBehind:Branch;
public branchOut:Subject<Out> = new Rx.Subject();
public thornOut:Subject<Out> = new Rx.Subject();
public flowerOut:Subject<Out> = new Rx.Subject();
public leafOut:Subject<Out> = new Rx.Subject();
constructor(stage:HTMLElement, settings:BranchSettings, grid:number, placeBehind:Branch = null)
{
this.grid = 50;//grid;
this.stage = stage;
this.placeBehind = placeBehind;
settings.width = 2;
settings.opacity = 1;
this.state = BranchState.animating;
let path = this.createLine(settings);
let branchCount:number = 2;
for(let i = 0; i < branchCount; i++)
{
this.createSqwig(i, branchCount, path, JSON.parse(JSON.stringify(settings)) as BranchSettings)
}
}
createSqwig(index:number, total:number, path:string, settings:BranchSettings)
{
let branch = document.createElementNS("http://www.w3.org/2000/svg", 'path')
branch.setAttribute('d', path)
branch.style.fill = 'none';
branch.style.stroke = this.getColor(index);
branch.style.strokeLinecap = "round";
settings.length = branch.getTotalLength();
settings.progress = settings.length;
branch.style.strokeDasharray= `${settings.length}, ${settings.length}`;
branch.style.strokeDashoffset = `${settings.length}`;
this.branches.push({path: branch, settings: settings});
if(!this.placeBehind) this.stage.appendChild(branch);
else this.stage.insertBefore(branch, this.placeBehind.branches[0].path)
let widthTarget = settings.sections * 0.8;
TweenMax.set(branch, {x: -index * 2, y: -index * 2})
TweenMax.to(settings, settings.sections * 0.4, {
progress: 0,
width: widthTarget,
ease: Power1.easeOut,
delay: index * (settings.sections * 0.001),
onUpdate: () =>
{
if(index == 0 && settings.sections > 4)
{
let choice = Math.random();
let length = settings.length - settings.progress;
let pos = branch.getPointAtLength(length);
let sec = Math.ceil((settings.progress / settings.length) * settings.sections) - 2;
if( sec < 4) sec = 4;
let out:Out = {
position: {x: pos.x, y: pos.y},
width: widthTarget,
sections: sec
}
if(choice < 0.02) this.branchOut.next(out)
else if(choice < 0.1) this.thornOut.next(out)
else if(choice < 0.2) this.flowerOut.next(out)
else if(choice < 0.4) this.leafOut.next(out)
}
},
onComplete: () =>
{
if(index = total - 1) this.state = BranchState.ended;
//branch.remove();
}
})
}
public update()
{
this.branches.map((set: BranchSet) =>
{
set.path.style.strokeDashoffset = `${set.settings.progress}`;
set.path.style.strokeWidth = `${set.settings.width}px`;
//set.path.style.opacity = `${set.settings.opacity}`;
})
}
private createLine(settings:BranchSettings):string
{
let x = settings.x;
let y = settings.y;
let dx = settings.directionX;
let dy = settings.directionY;
let path:string[] = [
'M',
'' + x,
'' + y
]
let steps = settings.sections;
let step = 0;
let getNewDirection = (direction: string, goAnywhere:boolean) =>
{
if(!goAnywhere && settings['direction' + direction.toUpperCase()] != 0) return settings['direction' + direction.toUpperCase()];
return Math.random() < 0.5 ? -1 : 1;
}
if(steps * 2 > step) path.push("Q")
while(step < steps * 2)
{
step++;
let stepUp = this.stepUp(step);
x += (dx * stepUp) * this.grid;
y += (dy * stepUp) * this.grid;
if(step != 1) path.push(',');
path.push('' + x);
path.push('' + y);
if(step % 2 != 0)
{
dx = dx == 0 ? getNewDirection('x', step > 8) : 0;
dy = dy == 0 ? getNewDirection('y', step > 8) : 0;
}
}
return path.join(' ');
}
private stepUp(step:number):number
{
let r = Math.random() * 10;
return step / (10 + r)
}
public clear()
{
this.branchOut.complete();
this.thornOut.complete();
this.leafOut.complete();
this.flowerOut.complete();
this.branches.map((set: BranchSet) => set.path.remove())
}
private getColor(index:number):string
{
let base = ['#646F4B']
let greens = ['#6FCAB1'];//, '#5DC4A8', '#4BBD9E', '#3AB795', '#A7CCBA', '#91C0A9', '#86BAA1']
let chooseFrom = index == 0 ? base : greens;
return chooseFrom[Math.floor(Math.random() * chooseFrom.length)];
}
}
class Flower
{
petals:SVGPathElement[] = [];
constructor(stage:HTMLElement, position: Position, size:number, colors:FlowerColors)
{
//outer petals
let petalCount = 8;
let p = petalCount;
let rotateAmount = 360 / petalCount;
let growRotation = (Math.random() * 120) - 60;
while(p > 0)
{
--p;
let petal = document.createElementNS("http://www.w3.org/2000/svg", 'path')
petal.setAttribute('d', this.createPetalPath({x: 0, y: 0}, size))
petal.setAttribute('class', 'petal')
petal.style.fill = colors.outer;
petal.style.stroke = 'none';
this.petals.push(petal);
let rotate = (rotateAmount * p) + Math.random() * 30;
TweenMax.set(petal, {scale:0, x: position.x, y: position.y, rotation: rotate})
let delay = Math.random();
TweenMax.to(petal, 1.5, {scale:1, delay: delay})
TweenMax.to(petal, 3, {rotation: '+=' + growRotation, delay: delay, ease: Elastic.easeOut})
stage.appendChild(petal);
}
// inner petals
petalCount = 6;
p = petalCount;
rotateAmount = 360 / petalCount;
while(p > 0)
{
--p;
let petal = document.createElementNS("http://www.w3.org/2000/svg", 'path')
petal.setAttribute('d', this.createPetalPath({x: 0, y: 0}, size / 2))
petal.setAttribute('class', 'petal')
petal.style.fill = colors.inner;
petal.style.stroke = 'none';
this.petals.push(petal);
let rotate = (rotateAmount * p) + Math.random() * 30;
TweenMax.set(petal, {scale:0, x: position.x, y: position.y, rotation: rotate})
TweenMax.to(petal, 6, {scale:1, rotation: '+=' + growRotation, delay: 1 + Math.random(), ease: Elastic.easeOut})
stage.appendChild(petal);
}
}
private createPetalPath(p: Position, size: number):string
{
let top = size * 4;
let middle = size * 1.8;
let width = size;
let path = `M ${p.x} ${p.y} Q ${p.x - width} ${p.y + middle} ${p.x} ${p.y + top} Q ${p.x + width} ${p.y + middle} ${p.x} ${p.y} Z`
return path;
}
public clear()
{
this.petals.map((petal: SVGPathElement) => petal.remove())
}
}
class Leaf
{
leaf:SVGPathElement;
constructor(stage:HTMLElement, position: Position, size:number)
{
this.leaf = document.createElementNS("http://www.w3.org/2000/svg", 'path')
this.leaf.setAttribute('d', this.createLeafPath({x: 0, y: 0}, size))
this.leaf.setAttribute('class', 'leaf')
this.leaf.style.fill = this.getColor();
this.leaf.style.stroke = 'none';
let rotate = Math.random() * 360;
let rotateGrow = (Math.random() * 300) - 150;
TweenMax.set(this.leaf, {scale:0, x: position.x, y: position.y, rotation: rotate})
TweenMax.to(this.leaf, 3, {scale:1})
TweenMax.to(this.leaf, 4, {rotation: rotate + rotateGrow, ease: Elastic.easeOut})
stage.appendChild(this.leaf);
}
private createLeafPath(p: Position, size: number):string
{
let top = size * (3 + Math.random() * 2);
let middle = size * (1 + Math.random());
let width = size * (1.5 + Math.random() * 0.5);
let path = `M ${p.x} ${p.y} Q ${p.x - width} ${p.y + middle} ${p.x} ${p.y + top} Q ${p.x + width} ${p.y + middle} ${p.x} ${p.y} Z`
return path;
}
private getColor():string
{
let greens = ['#00A676', '#00976C', '#008861', '#007956']
return greens[Math.floor(Math.random() * greens.length)];
}
public clear()
{
this.leaf.remove()
}
}
class Thorn
{
private thorn:SVGPathElement;
constructor(stage:HTMLElement, position: Position, size:number)
{
this.thorn = document.createElementNS("http://www.w3.org/2000/svg", 'path')
this.thorn.setAttribute('d', this.createThornPath({x: 0, y: 0}, size))
this.thorn.setAttribute('class', 'thorn')
this.thorn.style.fill = '#646F4B';
this.thorn.style.stroke = 'none';
TweenMax.set(this.thorn, {scale:0, x: position.x, y: position.y, rotation: Math.random() * 360})
TweenMax.to(this.thorn, 3, {scale:1})
stage.appendChild(this.thorn);
}
private createThornPath(p:Position, w:number):string
{
let path = `M ${p.x} ${p.y} Q ${p.x - w / 2} ${p.y} ${p.x - w / 2} ${p.y + w / 4} L ${p.x} ${p.y + w * 2} L ${p.x + w / 2} ${p.y + w / 4} Q ${p.x + w / 2} ${p.y} ${p.x} ${p.y} Z`
return path;
}
public clear()
{
this.thorn.remove();
}
}
class App
{
private container:HTMLElement;
private downloadButton:HTMLElement;
private svg:HTMLElement;
private branches:Branch[] = [];
private thorns:Thorn[] = [];
private flowers:Flower[] = [];
private leaves:Leaf[] = [];
private branchGroup:HTMLElement;
private thornGroup:HTMLElement;
private leafGroup:HTMLElement;
private flowerGroup:HTMLElement;
private width: number = 600;
private height: number = 600;
private lastMousePosition:Position;
private direction:Position;
private grid:number = 40;
private flowerColors:FlowerColors;
constructor(container:HTMLElement, downloadButton:HTMLElement)
{
this.container = container;
this.downloadButton = downloadButton;
this.svg = document.getElementById('stage');
this.branchGroup = document.getElementById('branchGroup');
this.thornGroup = document.getElementById('thornGroup');
this.leafGroup = document.getElementById('leafGroup');
this.flowerGroup = document.getElementById('flowerGroup');
this.onResize();
this.tick();
Rx.Observable.fromEvent(this.downloadButton, 'click')
.map((mouseEvent:MouseEvent) => {mouseEvent.preventDefault()})
.subscribe(() => this.download());
Rx.Observable.fromEvent(this.container, 'click')
.map((mouseEvent:MouseEvent) =>
{
mouseEvent.preventDefault();
return {
x: mouseEvent.clientX,
y: mouseEvent.clientY
};
})
.subscribe((position:Position) =>
{
this.clearOld();
this.startBranch(16, position, true);
});
this.startBranch(16, {x: this.width / 2, y: this.height / 2}, true);
Rx.Observable.fromEvent(window, "resize").subscribe(() => this.onResize())
}
private clearOld()
{
this.branches.map((branch:Branch) =>
{
branch.clear();
});
this.thorns.map((thorn:Thorn) => thorn.clear());
this.flowers.map((flower:Flower) => flower.clear());
this.leaves.map((leaf:Leaf) => leaf.clear());
TweenMax.killAll();
this.branches = [];
this.thorns = [];
this.flowers = [];
this.leaves = [];
}
private startBranch(sections:number, position:Position, setColors:boolean = false)
{
if(setColors)
{
this.flowerColors = {
outer: this.getColor(),
inner: this.getColor()
}
}
let dx = Math.random();
if(dx > 0.5) dx = dx > 0.75 ? 1 : -1;
else dx = 0;
let dy= 0;
if(dx == 0) dx = Math.random() > 0.5 ? 1 : -1;
let settings:BranchSettings = {
x: position.x,
y: position.y,
directionX: dx,
directionY: dy,
sections: sections
}
let newBranch = new Branch(this.branchGroup, settings, this.grid/2 + Math.random() * this.grid/2, this.branches.length > 1 ? this.branches[this.branches.length - 2]: null);
newBranch.branchOut.debounceTime(200).subscribe((out:Out) => this.startBranch(out.sections, out.position))
newBranch.thornOut.debounceTime(100).subscribe((out:Out) => this.thorns.push(new Thorn(this.thornGroup, out.position, out.width)))
newBranch.flowerOut.debounceTime(300).subscribe((out:Out) => this.flowers.push(new Flower(this.flowerGroup, out.position, out.width, this.flowerColors)))
newBranch.leafOut.debounceTime(50).subscribe((out:Out) => this.leaves.push(new Leaf(this.leafGroup, out.position, out.width)))
this.branches.push(newBranch);
}
private onResize()
{
this.width = this.container.offsetWidth;
this.height = this.container.offsetHeight;
this.svg.setAttribute('width', String(this.width));
this.svg.setAttribute('height', String(this.height));
}
private tick()
{
let step = this.branches.length - 1;
while(step >= 0)
{
if(this.branches[step].state != BranchState.ended)
{
this.branches[step].update();
}
--step;
}
requestAnimationFrame(() => this.tick());
}
private download()
{
var a = document.createElement('a'), xml, ev;
a.download = 'my-amazing-plant(by ste.vg).svg';
xml = (new XMLSerializer()).serializeToString(this.svg);
a.href = 'data:application/octet-stream;base64,' + btoa(xml);
ev = document.createEvent("MouseEvents");
ev.initMouseEvent("click", true, false, self, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(ev);
}
private getColor():string
{
let offset = Math.round(Math.random() * 100)
var r = Math.sin(0.3 * offset) * 100 + 155;
var g = Math.sin(0.3 * offset + 2) * 100 + 155;
var b = Math.sin(0.3 * offset + 4) * 100 + 155;
return "#" + this.componentToHex(r) + this.componentToHex(g) + this.componentToHex(b);
}
private componentToHex(c:number)
{
var hex = Math.round(c).toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
}
let container = document.getElementById('app');
let downlaodButton = document.getElementById('download-button');
let app = new App(container, downlaodButton);
<script src="//codepen.io/steveg3003/pen/zBVakw.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.0/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.0.1/Rx.min.js"></script>
@import url('https://fonts.googleapis.com/css?family=Libre+Baskerville');
html, body
{
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
user-select: none;
color: #111;
font-family: 'Libre Baskerville', serif;
}
$color-back: #f0f0f0;
$color-text: #303030;
$color-link: darken($color-back, 10%);
body
{
background-color: $color-back;
cursor: pointer;
}
#app
{
width: 100%;
height: 100%;
}
#stage
{
backface-visibility: visible;
}
#download-button
{
position: absolute;
bottom: 0;
left: 0;
padding: 10px;
display: inline-block;
margin: 10px;
background-color: $color-back;
color: $color-text;
border-radius: 4px;
cursor: pointer;
border-bottom: solid 3px $color-back;
&:hover
{
border-bottom: solid 3px $color-link;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment