<div id="app">
<div id="image"></div>
<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>
<div id="image-preload">
</div>
<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>
@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: #434343;
$color-text: #303030;
$color-link: darken($color-back, 10%);
body
{
background-color: $color-back;
cursor: pointer;
}
#app
{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
#image
{
width: 600px;
height: 350px;
background-color: $color-back;
background-position: center center;
background-size: cover;
transition: background-image 1s ease;
}
}
#stage
{
backface-visibility: visible;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#image-preload
{
visibility: none;
}
// 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, setPath:string = null)
{
this.grid = 50;//grid;
this.stage = stage;
this.placeBehind = placeBehind;
settings.width = 2;
settings.opacity = 1;
this.state = BranchState.animating;
let path = setPath ? setPath : 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() * 100) - 50;
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() * 20;
TweenMax.set(petal, {scale:0, x: position.x, y: position.y, rotation: rotate})
let delay = Math.random();
TweenMax.to(petal, 3, {scale:1, delay: delay})
TweenMax.to(petal, 6, {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() * 20;
TweenMax.set(petal, {scale:0, x: position.x, y: position.y, rotation: rotate})
TweenMax.to(petal, 12, {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() * 180) - 90;
TweenMax.set(this.leaf, {scale:0, x: position.x, y: position.y, rotation: rotate})
TweenMax.to(this.leaf, 4, {scale:1})
TweenMax.to(this.leaf, 6, {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 image:HTMLElement;
private imagePreload: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 framePath:string = 'M -300 -175, L 300 -175, 300 175, -300 175 Z';
private images:string[] = [
'https://images.unsplash.com/photo-1507835661088-ac1e84fe645f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=0e5105a10079b712ad6af13599b6fd91',
'https://images.unsplash.com/photo-1477414876610-1ec826f2e689?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=d2f06dfd48caa4a81416e0df9423b159',
'https://images.unsplash.com/photo-1518554372478-7ab26b40390d?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=9a9c1a52535cc81d89c0295c6cd4bdbd',
'https://images.unsplash.com/photo-1517867065801-e20f409696b0?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=886f14045400c50e383cd8739f4ea92f',
'https://images.unsplash.com/photo-1512144294577-9bbcd6ddd050?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=5202fd3453bcb4411e38c5d7c24f1318',
'https://images.unsplash.com/photo-1511283402428-355853756676?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=0ea1c1fdd2b64ea7f1e02842ac8a552b',
'https://images.unsplash.com/photo-1501845073335-1cb7bf68ff55?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=ef61da54e0f4ef147e5d1dc596237312',
'https://images.unsplash.com/photo-1487360920430-e18a62e59ad2?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=6435e6fb109da8af73deb458c5811ab6',
'https://images.unsplash.com/photo-1522230895200-b681389386d0?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=e4e38115195308d2fc63fb8624d931d7',
'https://images.unsplash.com/photo-1511988578842-d8abe0f6351a?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=9e58c4193f044dc404ca27c38bbb6115',
'https://images.unsplash.com/photo-1519645261061-3cee4d216668?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=060dff222881054806f948c50fb465f4',
'https://images.unsplash.com/photo-1467406821248-f0a1e57880bf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE0NTg5fQ&s=41a8eb1041b5c3db2debaddba134294b'
]
private selectedImage = Math.floor(Math.random() * this.images.length);
private flowerColors:FlowerColors;
constructor(container:HTMLElement)
{
this.container = container;
this.svg = document.getElementById('stage');
this.image = document.getElementById('image');
this.imagePreload = document.getElementById('image-preload');
//preload the images
this.images.map((image:string) => {
let img = document.createElement('img');
img.setAttribute('src', image);
this.imagePreload.append(img);
})
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.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.framePath);
});
this.clearOld()
this.startBranch(16, {x: this.width / 2, y: this.height / 2}, true, this.framePath);
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 = [];
this.selectedImage++;
if(this.selectedImage >= this.images.length) this.selectedImage = 0;
this.image.setAttribute('style', 'background-image: url(' + this.images[this.selectedImage] + ');')
}
private startBranch(sections:number, position:Position, setColors:boolean = false, setPath:string = null)
{
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, setPath);
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));
TweenMax.set(this.branchGroup, {x: this.width / 2, y: this.height / 2});
TweenMax.set(this.thornGroup, {x: this.width / 2, y: this.height / 2});
TweenMax.set(this.leafGroup, {x: this.width / 2, y: this.height / 2});
TweenMax.set(this.flowerGroup, {x: this.width / 2, y: this.height / 2});
}
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 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 app = new App(container);
Also see: Tab Triggers