<svg>
<!-- https://tympanus.net/codrops/2019/02/19/svg-filter-effects-creating-texture-with-feturbulence/ -->
<filter id='paper' x="0" y="0" width='100%' height="100%">
<feTurbulence type="fractalNoise" baseFrequency='0.05' result='noise' numOctaves="6" />
<feDiffuseLighting in='noise' lighting-color='white' surfaceScale='3'>
<feDistantLight azimuth='45' elevation='60' />
</feDiffuseLighting>
</filter>
</svg>
<div class="card">
<svg viewBox="0 0 200 200" class="stipple">
</svg>
</div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
background: #f4f4f4;
}
.card {
position: absolute;
width: 85vmin;
height: 85vmin;
padding: 2.5vmin;
grid-gap: 1vmin;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
background: #fff;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 1px solid #f1f1f1;
}
.card > div {
position: relative;
z-index: 1;
overflow: hidden;
transform: transalte3d(0px, 0px, 0px);
}
.card > div svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.stipple {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 1;
height: 100%;
}
const fillColor = "#111";
// Utils
function random(min, max) {
return Math.random() * (max - min) + min;
}
function map(n, start1, end1, start2, end2) {
return ((n - start1) / (end1 - start1)) * (end2 - start2) + start2;
}
function percentage(partialValue, totalValue) {
return (100 * partialValue) / totalValue;
}
function lerp(x1, y1, x2, y2, amt) {
const x = x1 + (x2 - x1) * amt;
const y = y1 + (y2 - y1) * amt;
return { x, y };
}
// Tree
class Tree {
constructor(x, y, SVGElement) {
this.x = x;
this.y = y;
this.initY = y;
this.canvas = SVG(SVGElement);
this.runDebug = false;
}
initialiseVars() {
this.width = random(60, 120);
this.height = random(80, 180);
this.y = this.initY - (200 - this.height) / 2;
this.trunkWidth = random(0.5, 1);
this.branchVerticalLevels = ~~random(6, 24);
const rotation = random(5, 10);
if (random(0, 1) > 0.5) {
this.branchUpwardsRotation = 100 - rotation;
} else {
this.branchUpwardsRotation = 100 + rotation;
}
}
render() {
this.clear();
this.initialiseVars();
if (this.runDebug) {
this.debug();
}
this.renderBranches();
this.renderTrunk();
}
renderTrunk() {
const numTrunks = ~~random(1, 5);
for (let i = 0; i < numTrunks; i++) {
const trunk = this.canvas
.rect(this.trunkWidth * random(0.5, 1), this.height)
.cx(this.x)
.y(this.y - this.height)
.rotate(random(-0.5, 0.5))
.fill(fillColor);
}
}
renderBranches() {
const bottomPadding = random(10, 30);
const branchVertInc =
(this.height - bottomPadding) / this.branchVerticalLevels;
const maxBranchWidth = this.width / random(2, 3);
const minBranchWidth = this.width / random(12, 24);
this.maxBranchWidth = maxBranchWidth;
this.minBranchWidth = minBranchWidth;
for (let i = 0; i < this.branchVerticalLevels; i++) {
const branchWidth = map(
i,
0,
this.branchVerticalLevels - 1,
maxBranchWidth,
minBranchWidth
);
const yPos = this.y - bottomPadding - branchVertInc * i;
// left
this.branch(this.x, yPos, this.x - branchWidth, yPos, i, "left");
// right
this.branch(this.x, yPos, this.x + branchWidth, yPos, i, "right");
}
}
branch(x1, y1, x2, y2, level, side) {
const branch = this.canvas.group();
y2 = percentage(y2, this.branchUpwardsRotation);
const numSubBranches = ~~random(10, 30);
for (let i = 0; i < numSubBranches; i++) {
const startPos = random(0, 0.75);
const len = random(0.125, 0.375);
const start = lerp(x1, y1, x2, y2, startPos);
const end = lerp(x1, y1, x2, y2, startPos + len);
const size = Math.hypot(x2 - x1, y2 - y1);
const maxRotation = map(
size,
this.minBranchWidth,
this.maxBranchWidth,
2,
8
);
const startX = start.x;
const startY = start.y;
const endX = end.x;
const endY = end.y + random(-maxRotation, maxRotation);
const line = branch.line(startX, startY, endX, endY).stroke({
width: this.trunkWidth * random(0.25, 1),
color: fillColor
});
if (random(0, 1) > 0.99) {
this.canvas
.ellipse(random(1, 2), random(1, 2))
.cx(endX)
.cy(endY)
.fill("#ef233c")
.opacity(random(0, 1));
} else {
if (random(0, 1) > 0.99) {
this.canvas
.circle(random(1, 2), random(1, 2))
.cx(startX)
.cy(startY)
.fill("#ef233c")
.opacity(random(0, 1));
}
}
}
}
clear() {
// this.canvas.clear();
}
debug() {
this.canvas
.rect(this.width, this.height)
.cx(this.x)
.y(this.y - this.height)
.fill("rgba(255, 0, 0, 0.125)");
}
}
setInterval(() => {
render();
}, 2000);
function render() {
const trees = [];
const cellSize = ~~random(1, 4);
const numTrees = cellSize * cellSize;
const card = document.querySelector(".card");
card.innerHTML = '<svg viewBox="0 0 200 200" class="stipple"></svg>';
card.style.gridTemplateColumns = `repeat(${cellSize}, 1fr)`;
card.style.gridTemplateRows = `repeat(${cellSize}, 1fr)`;
const stippleCanvas = SVG(".stipple");
stipple();
for (let i = 0; i < numTrees; i++) {
const el = document.createElementNS("http://www.w3.org/2000/svg", "svg");
el.setAttribute("viewBox", "0 0 200 200");
const wrapper = document.createElement("div");
wrapper.appendChild(el);
card.appendChild(wrapper);
const tree = new Tree(100, 200, el);
trees.push(tree);
tree.render();
}
function stipple() {
const r = stippleCanvas.rect(200, 200).fill("#fff");
r.node.setAttribute("filter", "url(#paper)");
r.opacity(random(0.4, 0.625));
stippleCanvas.node.style.opacity = random(0.3, 0.6);
for (let i = 0; i < 150; i++) {
const snowFlake = stippleCanvas
.ellipse(random(0.1, 0.5), random(0.1, 0.5))
.cx(random(0, 200))
.cy(random(0, 200))
.fill(fillColor);
}
}
}
render();
This Pen doesn't use any external CSS resources.