Skip to content

Commit

Permalink
Content: Add Propellers doodle to homepage
Browse files Browse the repository at this point in the history
  • Loading branch information
rileyjshaw committed Jan 15, 2024
1 parent b53a3d9 commit b004a13
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 14 deletions.
2 changes: 2 additions & 0 deletions src/components/GridDoodles.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CycleText from './CycleText';
import BackgroundGenerator from './doodles/BackgroundGenerator';
import Propellers from './doodles/Propellers';
import CircleConstrainedLines from './doodles/CircleConstrainedLines';
import GameOver from './doodles/GameOver';
import Riot from './doodles/Riot';
Expand All @@ -14,6 +15,7 @@ export default [
)),
0.8,
],
[React.forwardRef((_, ref) => <Propellers El="li" ref={ref} />), 0.8],
[
React.forwardRef((_, ref) => (
<CircleConstrainedLines El="li" ref={ref} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/doodles/CircleConstrainedLines.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.doodle-constrained-lines {
align-items: center;
background: var(--color-red-600);
background: var(--color-grey-500);
display: flex;
grid-area: span 3 / span 3;
justify-content: center;
Expand Down
45 changes: 34 additions & 11 deletions src/components/doodles/CircleConstrainedLines.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import './CircleConstrainedLines.css';
import React, {useRef, useState, useEffect, useMemo} from 'react';

const {PI, cos, sin, tan, pow} = Math;
const L = 800;
const R = L / 2.5;
const SIZE_UNSCALED = 800;
const R_UNSCALED = SIZE_UNSCALED / 2.5;

// Derived.
const C = L / 2;
const C_UNSCALED = SIZE_UNSCALED / 2;

const variants = [
{
Expand Down Expand Up @@ -85,12 +85,34 @@ const variants = [
];

const CircleConstrainedLines = React.forwardRef(
function CircleConstrainedLines({El = 'div', settings: {theme}}, ref) {
function CircleConstrainedLines(
{El = 'div', settings: {theme}, onFullCycle},
ref,
) {
const canvasRef = useRef(null);
const [variant, setVariant] = useState(
const initialVariantIdxOffset = useRef(
Math.floor(Math.random() * variants.length),
);
const [variantIdxOffset, setVariantIdxOffset] = useState(0);
const themeColors = useMemo(() => ABSTRACT_COLORS[theme], [theme]);

useEffect(() => {
if (
variantIdxOffset > 0 &&
variantIdxOffset % variants.length === 0
) {
onFullCycle?.();
}
}, [variantIdxOffset]);

const variantIdx =
(initialVariantIdxOffset.current + variantIdxOffset) %
variants.length;

const SIZE = SIZE_UNSCALED * window.devicePixelRatio;
const R = R_UNSCALED * window.devicePixelRatio;
const C = C_UNSCALED * window.devicePixelRatio;

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
Expand All @@ -100,14 +122,14 @@ const CircleConstrainedLines = React.forwardRef(
nLines,
lineWidth = 1,
globalCompositeOperation = 'source-over',
} = variants[variant];
} = variants[variantIdx];

const angleStep = (2 * PI) / nLines;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.globalCompositeOperation = globalCompositeOperation;
ctx.lineWidth = lineWidth;
ctx.lineWidth = window.devicePixelRatio * lineWidth;
ctx.strokeStyle = themeColors.fg;
ctx.fillStyle = themeColors.bg;
ctx.arc(C, C, R * 1.03, 0, 2 * PI);
Expand All @@ -130,17 +152,18 @@ const CircleConstrainedLines = React.forwardRef(
ctx.lineTo(C + x2, C + y2);
ctx.stroke();
}
}, [variant, theme]);
}, [variantIdx, theme]);

return (
<El
{...(ref?.hasOwnProperty('current') ? {ref} : {})}
className="content-node doodle doodle-constrained-lines"
>
<canvas
height={L}
width={L}
height={SIZE}
width={SIZE}
ref={canvasRef}
onClick={() => setVariant(v => (v + 1) % variants.length)}
onClick={() => setVariantIdxOffset(v => v + 1)}
/>
</El>
);
Expand Down
16 changes: 16 additions & 0 deletions src/components/doodles/Propellers.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.doodle-propellers {
align-items: center;
background: var(--color-grey-500);
display: flex;
grid-area: span 3 / span 3;
height: 100%;
justify-content: center;
}
.doodle-propellers canvas {
background: var(--color-bg);
border: 4px solid var(--color-fg);
border-radius: 50%;
cursor: pointer;
max-height: 86%;
max-width: 86%;
}
164 changes: 164 additions & 0 deletions src/components/doodles/Propellers.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {ABSTRACT_COLORS} from '../../util/constants';
import {withSettings} from '../SettingsProvider';
import './Propellers.css';
import React, {useRef, useState, useEffect, useMemo} from 'react';

const SIZE_UNSCALED = 520;

// Helper function: triangle wave.
function tri(x) {
return 1 - 2 * Math.abs(Math.round(x / 2) - x / 2);
}

const variants = [
{
nLines: 10,
lineWidth: 4,
lineLengthFactor: 1,
speed: 5,
getAngle: (t, isOdd) =>
(Math.PI / 2) * (tri(t / Math.PI) + isOdd + 0.5),
},
{
nLines: 8,
lineWidth: 20,
lineLengthFactor: 1 / 1.5,
speed: 2,
getAngle: (t, isOdd) => t + isOdd + 0.5,
},
{
nLines: 13,
lineWidth: 3,
lineLengthFactor: 1 / 1.6,
speed: 10 / 3,
getAngle: (t, isOdd, x, y, nLines) =>
(Math.PI / 2) * (t + isOdd + (x + y * 4) / nLines),
},
{
nLines: 13,
lineWidth: 80 / 1.92,
lineLengthFactor: 1 / 1.92,
speed: 10 / 3,
globalCompositeOperation: 'xor',
isStarkClear: true,
getAngle: (t, isOdd) => t + isOdd + 0.5,
},
];

const Propellers = React.forwardRef(function Propellers(
{El = 'div', settings: {theme}, onFullCycle},
ref,
) {
const SIZE = SIZE_UNSCALED * window.devicePixelRatio;

const animationFrameRef = useRef(null);
const canvasRef = useRef(null);
const settingsRef = useRef({colors: null, variant: null});
const themeColors = useMemo(() => ABSTRACT_COLORS[theme], [theme]);

const initialVariantIdxOffset = useRef(
Math.floor(Math.random() * variants.length),
);
const [variantIdxOffset, setVariantIdxOffset] = useState(0);

useEffect(() => {
if (variantIdxOffset > 0 && variantIdxOffset % variants.length === 0) {
onFullCycle?.();
}
}, [variantIdxOffset]);

const variantIdx =
(initialVariantIdxOffset.current + variantIdxOffset) % variants.length;

useEffect(() => {
settingsRef.current.colors = themeColors;
}, [themeColors]);

useEffect(() => {
settingsRef.current.variant = variants[variantIdx];
}, [variantIdx]);

useEffect(() => {
function draw() {
animationFrameRef.current = window.requestAnimationFrame(draw);

const canvas = canvasRef.current;
if (!canvas) return;

const {
colors,
variant: {
nLines,
speed,
lineWidth = 1,
lineLengthFactor = 3 / 4,
globalCompositeOperation = 'source-over',
isStarkClear,
getAngle,
},
} = settingsRef.current;

// Derived constants.
const lineSpacing = SIZE / nLines;
const lineLength = lineSpacing * lineLengthFactor;
const centerSpacings = Array.from(
{length: nLines},
(_, n) => (n + 0.5) * lineSpacing,
);

const ctx = canvas.getContext('2d');
ctx.fillStyle = `${colors.bg}2a`;
ctx.strokeStyle = colors.fg;
ctx.lineWidth = window.devicePixelRatio * lineWidth;
ctx[isStarkClear ? 'clearRect' : 'fillRect'](
0,
0,
canvas.width,
canvas.height,
);
ctx.globalCompositeOperation = globalCompositeOperation;

// Animation starts here.
const t = (Date.now() / 1000) * speed;
for (let y = 0; y < nLines; ++y) {
const cy = centerSpacings[y];
for (let x = 0; x < nLines; ++x) {
const isOdd = (x + y) % 2;
const direction = isOdd ? 1 : -1;

const angle = getAngle(t, isOdd, x, y, nLines);

const cx = centerSpacings[x];
const dx = Math.cos(angle) * lineLength;
const dy = Math.sin(angle) * lineLength * direction;

ctx.beginPath();
ctx.moveTo(cx - dx, cy - dy);
ctx.lineTo(cx + dx, cy + dy);
ctx.stroke();
}
}
ctx.restore();
}
animationFrameRef.current = window.requestAnimationFrame(draw);
return () => {
window.cancelAnimationFrame(animationFrameRef.current);
};
}, []);

return (
<El
{...(ref?.hasOwnProperty('current') ? {ref} : {})}
className="content-node doodle doodle-propellers"
>
<canvas
height={SIZE}
width={SIZE}
ref={canvasRef}
onClick={() => setVariantIdxOffset(i => i + 1)}
/>
</El>
);
});

export default withSettings(Propellers);
15 changes: 13 additions & 2 deletions src/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@ import allProjectsQuery from '../util/all-projects-query';
import {sortByDate} from '../util/sorting-methods';
import './index.css';
import {Link} from 'gatsby';
import React from 'react';
import React, {useCallback, useState} from 'react';
import GalleryImage from '../components/GalleryImage';
import {StaticImage} from 'gatsby-plugin-image';
import {ExternalLink} from '../components/AutoLink';
import BackgroundGenerator from '../components/doodles/BackgroundGenerator';
import Riot from '../components/doodles/Riot';
import CircleConstrainedLines from '../components/doodles/CircleConstrainedLines';
import Propellers from '../components/doodles/Propellers';
import PinkNoise from '../components/PinkNoise';
import {DIRECT_COLORS} from '../util/constants.mjs';

export const Head = SEO;

const DOODLES = [CircleConstrainedLines, Propellers];

const IndexPage = ({featuredProjects = []}) => {
const [featuredDoodleIdx, setFeaturedDoodleIdx] = useState(0);
const incrementFeaturedDoodleIdx = useCallback(() => {
setFeaturedDoodleIdx(i => (i + 1) % DOODLES.length);
}, []);
const FeaturedDoodle = DOODLES[featuredDoodleIdx];

return (
<main>
<div className="page-content">
Expand Down Expand Up @@ -99,7 +108,9 @@ const IndexPage = ({featuredProjects = []}) => {
/>
</li>
<li className="project no-zoom">
<CircleConstrainedLines />
<FeaturedDoodle
onFullCycle={incrementFeaturedDoodleIdx}
/>
</li>
<li>
<ul className="bento-half-x">
Expand Down

0 comments on commit b004a13

Please sign in to comment.