Skip to content

Commit

Permalink
feat: add animated background (#3266)
Browse files Browse the repository at this point in the history
* Add animated background

* fix footer logo collision

* fix section height and account for header and footer

* improve background animation

- Switch to ref directive to target canvas element
- Use built-in rAF timing
- Fix slow frametime rejection
  • Loading branch information
radium-v authored and chrisdholt committed Jun 16, 2020
1 parent 2cc5f9e commit d29f2ee
Show file tree
Hide file tree
Showing 6 changed files with 388 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const BackgroundDesignStyles = css`
width: 100%;
}
:host .background-image svg {
:host .background-image canvas {
height: auto;
min-width: 1440px;
object-fit: cover;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { html } from "@microsoft/fast-element";
import BackgroundImage from "svg/background.svg";
import { html, ref } from "@microsoft/fast-element";
import { BackgroundDesign } from "./background-design";

export const BackgroundDesignTemplate = html<BackgroundDesign>`
<template>
<div class="background-image">
${BackgroundImage}
<canvas ${ref("canvas")}></canvas>
</div>
</template>
`;
Original file line number Diff line number Diff line change
@@ -1,3 +1,260 @@
import { FASTElement } from "@microsoft/fast-element";
import { DesignSystemProvider } from "@microsoft/fast-foundation";
import { waveData } from "../../data/wave.data";

export class BackgroundDesign extends FASTElement {}
type BezierCurveTo = [number, number, number, number, number, number];
type MoveTo = [number, number];

interface PathData {
C: BezierCurveTo[];
color?: [number, number, number];
M: MoveTo;
}

export class BackgroundDesign extends FASTElement {
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
provider: DesignSystemProvider;

waveSim = {
scale: 1.0,
size: { width: 0.0, height: 0.0 },
center: { x: 0, y: 0 },
origin: { x: 0, y: 0 },
percent: { x: 0.5, y: 0.5 },
};

time = {
loop: 0.0,
scale: 1.0,
speed: 0.35,
total: 0.0,
};

increments = {
bg: 20,
waves: 30,
};

steps = {
bg: [],
waves: [],
};

lineWidths = {
bg: 1.5,
waves: 10.0,
};

waveData = {};

frame: number;

prevPerf: number = 0;

connectedCallback() {
super.connectedCallback();
this.provider = DesignSystemProvider.findProvider(this) as DesignSystemProvider;
this.context = this.canvas.getContext("2d", {
alpha: false,
}) as CanvasRenderingContext2D;

this.setup();
this.reflowCanvas();

let resizeTimeout;

window.addEventListener(
"resize",
() => {
if (resizeTimeout) {
resizeTimeout = clearTimeout(resizeTimeout);
}
resizeTimeout = window.setTimeout(() => this.reflowCanvas(), 100);
},
false
);

const performAnimation = perf => {
const frametime = perf - this.prevPerf;
this.update(frametime);
this.frame = requestAnimationFrame(performAnimation);
this.prevPerf = perf;
};
performAnimation(window.performance.now());
}

setup() {
Object.entries(waveData.layers).map(([key, list]) =>
list.map(({ from, to, color }, i) => {
const waveBlendData = {
path: this.convertPath(from, color),
start: this.convertPath(from),
end: this.convertPath(to),
};
if (!this.waveData.hasOwnProperty(key)) {
this.waveData[key] = [];
}
this.waveData[key].push(waveBlendData);
})
);
}

convertPath(rawPath, color?: number[]): PathData {
let M: MoveTo | undefined;
let C: BezierCurveTo[] | MoveTo | undefined = rawPath
.split(/(?=[MCS])/)
.map(d => {
let points = d
.slice(1, d.length)
.split(",")
.map(p => ~~p);

let pairs: number[] = [];
if (points.length === 2) {
M = points.map(p => ~~p) as MoveTo;
return;
}
for (let i = 0; i < points.length; i++) {
pairs.push(~~points[i]);
}
return pairs as BezierCurveTo | MoveTo;
})
.filter(Boolean);

return { C, M, ...(color && { color }) } as PathData;
}

stepPoint(from, to, increments, step) {
return from + ((to - from) / increments) * step;
}

generateWave(from, to, cache, inc) {
let colorBlend: number = 0;
for (let step = 0; step < inc; step++) {
colorBlend += 1 / inc;
const newWave = {
color: from.color.map(
(c, i) => c * colorBlend + (1 - colorBlend) * to.color[i]
),
C: from.C.map((c, n) =>
c.map((v, i) => this.stepPoint(v, to.C[n][i], inc, step))
),
M: from.M.map((p, i) => this.stepPoint(p, to.M[i], inc, step)),
};

cache.push(newWave);
}
}

updateWave(thisWave, startPoints, endPoints, modifier) {
// sin wave drives animation
let blend = this.easeInOut(1)(Math.sin(this.time.total / 2 + modifier));
for (let i = 0; i < thisWave.C.length; i++) {
const points = thisWave.C[i];
for (let j = 0; j < points.length; j++) {
let pointStart = startPoints.C[i][j];
let pointEnd = endPoints.C[i][j];
thisWave.C[i][j] =
((blend + 1) * (pointEnd - pointStart)) / 2 + pointStart;
}
}

return thisWave;
}

draw({ color, C, M }, lineWidth) {
this.context.beginPath();
this.context.moveTo(M[0], M[1]);

for (let i = 0; i < C.length; i++) {
this.context.bezierCurveTo(...(C[i] as BezierCurveTo));
}

this.context.lineWidth = lineWidth;
this.context.strokeStyle = `rgb(${color!.join(",")})`;
this.context.stroke();
this.context.closePath();
}

update(frametime) {
// you're too slow!
if (frametime > 33.34) {
return;
}

this.time.loop = frametime / 1000;
this.time.scale = this.time.loop * 60.0 * this.time.speed;
this.time.total += this.time.loop * this.time.scale;

this.steps.bg.length = 0;
this.steps.waves.length = 0;

this.context.fillStyle = this.provider.designSystem["backgroundColor"];
this.context.fillRect(0, 0, this.waveSim.size.width, this.waveSim.size.height);
this.context.save();
this.context.translate(
~~((this.waveSim.size.width - waveData.viewbox.width) / 2),
~~((this.waveSim.size.height - waveData.viewbox.height) / 2)
);

const keys = Object.keys(this.waveData);

// for-loops for performance
for (let g = 0; g < keys.length; g++) {
const key = keys[g];
const waves = this.waveData[key];

for (let i = 0; i < waves.length; i++) {
let wave = waves[i];
const modifier = i * 2 + (g + 1);
if (i < waves.length - 1) {
this.generateWave(
this.waveData[key][i].path,
this.waveData[key][i + 1].path,
this.steps[key],
this.increments[key]
);
}
wave = this.updateWave(wave.path, wave.start, wave.end, modifier);
}

const steps = this.steps[key];
for (let i = 0; i < steps.length; i++) {
const step = steps[i];
this.draw(step, this.lineWidths[key]);
}
}

this.context.restore();
}

easeIn = p => t => Math.pow(t, p);
easeOut = p => t => 1 - Math.abs(Math.pow(t - 1, p));
easeInOut = p => t =>
t < 0.5 ? this.easeIn(p)(t * 2) / 2 : this.easeOut(p)(t * 2 - 1) / 2 + 0.5;

reflowCanvas() {
let styles = window.getComputedStyle(this);

const w = parseInt(styles.getPropertyValue("width"), 10);
const h = parseInt(styles.getPropertyValue("height"), 10);

this.waveSim.scale = w / waveData.viewbox.width;

this.waveSim.size.width = w / this.waveSim.scale;
this.waveSim.size.height = h / this.waveSim.scale;

this.canvas.style.width = `${this.waveSim.size.width * this.waveSim.scale}px`;
this.canvas.style.height = `${this.waveSim.size.height * this.waveSim.scale}px`;
this.canvas.width = this.waveSim.size.width;
this.canvas.height = this.waveSim.size.height;

this.waveSim.origin.x = Math.ceil(
this.waveSim.size.width * this.waveSim.percent.x
);
this.waveSim.origin.y = Math.ceil(
this.waveSim.size.height * this.waveSim.percent.y
);
}
}
27 changes: 24 additions & 3 deletions sites/fast-website/src/app/css/style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:root {
--navbar-height: 94px;
}

html {
font: 16px "Segoe UI", Arial, Helvetica, sans-serif;
}
Expand Down Expand Up @@ -49,7 +53,7 @@ body {
.social-links {
top: 35%;
}

.section-scroll {
top: 25%;
}
Expand All @@ -61,7 +65,7 @@ body {

.header site-navigation {
align-items: center;
height: 94px;
height: var(--navbar-height);
justify-content: space-between;
}

Expand Down Expand Up @@ -95,7 +99,13 @@ site-navigation ::part(content) {
line-height: var(--type-ramp-plus-4-line-height);
}

.footer .logo::part(content) {
display: flex;
overflow: hidden;
}

.logo svg {
display: block;
width: 45px;
height: 40px;
margin-inline-end: 10px;
Expand Down Expand Up @@ -134,14 +144,24 @@ fast-divider::after {

.section {
color: var(--neutral-foreground-rest);
min-height: 100vh;
display: flex;
min-height: 100vh;
justify-content: center;
align-items: center;
grid-column: 2;
box-sizing: border-box;
}

.hero-section {
margin-top: calc(var(--navbar-height) * -1);
padding-top: var(--navbar-height);
}

.community-section {
margin-bottom: calc(var(--navbar-height) * -1);
padding-bottom: var(--navbar-height);
}

.fast-frame-section {
color: var(--neutral-foreground-rest);
min-height: 85vmin;
Expand Down Expand Up @@ -190,6 +210,7 @@ fast-divider::after {
}

.footer {
min-height: var(--navbar-height);
grid-column: 2;
display: flex;
align-items: center;
Expand Down
Loading

0 comments on commit d29f2ee

Please sign in to comment.