Cover image generated by Dall-E
We can reduce the new design problem to a simpler one: six points on a wheel that spin given a changing integer value. So, in this post I will document the work that was done to create the simpler mechanism, in React, and at the end summarize how it fits into the new design.
First, let's start with the view (fig. 1): a circle with six points sitting on the circumference, equidistant from each other. To produce such an image using CSS, I determined the coordinates of each of these six points using trigonometry.
Fig. 1
Given a circle with origin (0,0)
in cartesian coordinates, any point P
on the circumference of this circle can be described in terms of radius r
, and the degrees that a line from the origin to point P
would be rotated from the base line, which we define as a line from the origin to (∞,0)
(i.e. line going infinitely eastward). Since we have a fixed size circle, we know the radius r
. We also know where on the circumference we want to position the points, which in this case will be 6 equidistant points. Creating an imaginary line from each of these points to the origin will result in 6 lines (which we will sometimes refer to as spokes, as in "spokes" on a wheel), with 6 different angles between each of the them and the base line. In polar coordinates, each of these points can be notated as r∠θ where θ
is the angle between the spoke and the base line, but this method of positioning isn't usable in CSS, so we need to convert these into the more applicable cartesian coordinates using cosine and sine functions to be able to draw and position them with precision on the page.
Let's review the cosine and sine functions. They are based on an angle created by two lines, where one is the control line. What cosine of the angle between two lines produces is a quotient x/r
, where r
is the distance of the non-control line that starts from the intersection and ends at some point C
, from which drawing a straight line to intersect with the control line creates a right angle at that intersection. Then x
is the position along the control line relative to the intersection point that the above right angle would occur. Similarly, sine produces y/r
, where r
is the same as above (the distance along the non-control line that reaches point C
), and y
is the position along the line perpendicular to the control line relative to the intersection point such that when located based off the point at which the right angle occurs, it will intersect with the non-control line exactly at point C
, i.e. the point along the non-control line that is r
distance away from the intersection. Using the same two lines, point C
, x
, y
, r
, and the right angle are shown in Fig. 2.
Fig. 2
These functions can be applied to the context of a circle to find the (x,y) cartesian coordinates of points along the circumference. Knowing the rotation in degrees of the spoke (relative to the base line) that is connected to the point on the circumference, and the radius of the circle (i.e. spoke length), we can think of the base line (the one that starts at the origin of the circle and goes infinitely east on the x-axis) as the control line and use the spoke as the "other line" in the cosine and sine functions, making (0,0)
, the origin, where the two lines intersect. Then, the x
value in the cosine function will determine the x-coordinate of the point in question since x
in this case is the position relative to (0,0)
along the base line (which is also the x-axis) from where a perpendicular line passes through with the point in question. Similarly, we can use the y
value in the sine function to find the displacement along the y-axis from the origin since y
represents the position relative to the origin on the axis perpendicular to the base line, such that, when displaced by that amount from where the right angle occurs, the point in question will be reached. Formally:
Therefore, the (x,y) coordinate of a point on the circumference of a circle with spoke rotation θ
and radius r
(i.e. r∠θ) is:
As a quick aside, what do negative x
and y
values mean? Since x
and y
values that are found in the cosine and sine functions represent relative positions based on the intersection point, positive x
values can be seen as the distance needed to travel in one direction, and negative values the other. A positive x
value is a distance along the control line in the direction that the angle touches*, whereas a negative value is the distance traveled the opposite way. For y
values, a positive value is the distance along the perpendicular line in the direction of the angle*, and negative is the opposite way. Because of this, the definitions of cosine and sine described above are valid for all angles from , which follows that they are also valid for all quadrants of a circles, since the degrees that can be formed with two lines on a circle are contained within 0 to 360 degrees.
*"Angle touches" refers to the half of the line where an angle is formed. For example, take the following two angles that are formed by two lines:
Fig. 2
and are angle formed by lines A and B intersecting each other. However, they are formed on opposite halves of line B. In other words, "touches" one half of line B, and touches the other.
With this, we first define six equidistant points on the circumference of a circle by partitioning the available 360 degrees into 6 resulting in 60 degree separation between every two consecutive points. Then starting arbitrarily at 0 degrees, the enumeration of the degrees of the six points becomes 0, 60, 120, 180, 240, 300, which on a circle with radius r
, with the base line as the x-axis, their cartesian coordinates can be calculated as
, , ...,
Back to the CSS
When placing an absolute
positioned element within a <div>
, top
and left
properties can be used to specify where in the containing block it should be placed. They work similarly to cartesian coordinates in that the left
value is akin to the position on the x-axis while the top
value can be mapped to the position on the y-axis. "But in what direction?", you may ask. For the left
value, it is how many, say, pixels to the right the element should be placed, and for the top
value, it is how far to the bottom it should be placed. In other words, increasing left
values will place things more and more to the right of the page, and increasing top
values will place things more and more to the bottom of the page. Directionally, left
and the x-axis are equivalent, but top
and the y-axis are inverses.
If we want to use the results from the cosine and sine functions as shown above for the 6 points, let's consider the orientation of the control line, the "other" lines, and which angle to choose, such that we reduce the amount of arithmetic work necessary when determining the left
and top
css coordinates. Since the direction of the x
and y
values in the cosine and sine functions are dependent on the which half of the line the angle touches, we want choose the angle such that the direction matches the way the axes are oriented in css. Taking the cosine of angles on the right half of the base line will make it so that the greater the x
value, the farther along the base line the position will be. This direction matches the direction of observed in the left
property, establishing that the base line should be from (0,0)
to (∞,0)
. Now, given any "other" line (i.e. any spoke) that is not a copy of the base line, there are two ways to create an angle with the base line: above the base line or below the base line. Above the base line will yield y
values that increase as you go up the line perpendicular to the base line, while establishing the angle below the base line will yield the opposite, where positive y
values will indicate positions below the base line along the perpendicular line. Since the direction of the top
value is top-down, we want to choose the angle that is created below the base line, so that higher values will places things further down the page.
Lastly, the converted x
and y
values in the way described so far are based off of the origin of the circle. Because absolute
positioned <div>
s are by default at the top-left corner of the containing block, all the points drawn this way using will assume the top-left corner as the origin of the circle. For this, appropriate offset needs to be added to the left
and top
values in practice. If you want your circle to be just inside the containing <div>
, this can be done by simply adding the radius r
to the left
and top
values.
Coding it - The Wheel
The Background
We are now ready to draw the elements on the page. First let's create a react component to house these elements called wheel.js
.
The top level component in this react component is a <div>
positioned relative
that will act as the container of the circle points. Then, the 6 points are placed inside this top container <div>
, each a <div>
themselves with absolute
position. For <div>
s with absolute
positions to work as expected, the containing block has to be a non-static
position, which in this case will be relative
, or else they will default to the next non-static
containing block.
Each of the points will have different spoke angles which we store in an array. The 6 angles will be [0, 60, 120, 180, 240, 300]
. And each of them will have left
and top
values derived arithmetically from the expressions and , respectively, where θ
is their spoke angle and r
is the desired radius.
Lastly, to allow the points to move/spin along the circumference, we would need to allow the thetas to be dynamic and change given some external input. For now, let's create a prop that the react component can take in that defaults to 0, and add it to the thetas when being converted to their css coordinates.
The Code
First, let's define a function that does the coordinate calculation, of which the input parameters are the angle, theta
, and radius r
.
const convertX = (theta, radius) =>
radius + radius * Math.cos(theta * Math.PI / 180);
const convertY = (theta, radius) =>
radius + radius * Math.sin(theta * Math.PI / 180);
Math.cos
andMath.sin
expect the input, the angle, to be in radians, which is converted from degrees by multiplying then dividing by 180.
From there, in the react component markup, inside the containing <div>
, we can loop through the 6 angles and render a <div>
for each of them that represents a point on the page. For debugging convenience, we'll output the respective theta value in each of these point <div>
s.
export default function Wheel({ change = 0 }) {
const thetas = [0, 60, 120, 180, 240, 300];
const radius = 200 // pixels
return (
// containing block
<div className="wheelContainer">
{thetas.map((theta, index) => (
<div
key={index}
className="point"
style={{
left: convertX(theta + change, radius),
top: convertY(theta + change, radius)
}}
>
{theta}
</div>
))}
<div
className="circle"
style={{ width: radius * 2, height: radius * 2 }}
/>
</div>
);
}
Finally, we add the styles to the wheelContiner
<div>
and point
<div>
s.
.wheelContainer {
position: relative;
max-width: 500px;
max-height: 500px; // so it isn't massive on your 4000 pixel monitors
}
.point {
position: absolute;
border: black 1px solid;
}
.circle {
border: black 1px solid;
border-radius: 9999px;
}
Fig. 4
Fig. 4 shows the react component rendered.
Adding Spin
In App.js
, we will embed the Wheel
component, but also create a scrollpad that can be scrolled by the user's scroll wheel or trackpad. It will act as the input mechanic for indicating how much the points on the wheel will spin. The scrollpad will be housed in a separate <div>
and an onScroll
event listener will be attached to it that'll keep track of the degrees by which the points should rotate. The scrollpad will consist of a <div>
that will be of a fixed size, and inside that will be another <div>
with a much taller height than the containing <div>
. Since the containing <div>
will have a fixed height with overflow: scroll
, it will be scrollable by the user. Finally, the amount scrolled by the user will be tracked in a state variable and passed into the Wheel
component's change
property.
// App.js
import { useState } from "react";
import "./App.css";
import Wheel from "./components/Wheel";
export default function App() {
const [travel, setTravel] = useState(0);
const onScrollChange = (event) => {
const n = event.target.scrollTop % 360;
setTravel(n < 0 ? 360 + n : n);
};
return (
<div className="App">
<header className="App-header">
<Wheel change={travel} />
<h3>Scrollpad</h3>
<div className="scroller" onScroll={onScrollChange}>
<div className="overflower" />
</div>
</header>
</div>
);
}
The below css finalizes the implementation of the scroller.
.scroller {
width: 400px;
height: 300px;
overflow: scroll;
border: #000000 1px solid;
}
.overflower {
width: 100%;
height: 2000px;
}
The Demo
Scrollpad
Use in New Design
The underlying mechanism of the new design is based on this spinning wheel component. Instead of the 6 points, I am rendering on the circumference of an imaginary circle 6 blog posts. Then, stretching out the size of the circle to expand past the viewport allows each post on the circumference to occupy one entire page, and as the posts rotate on the wire, a post leaves the screen as a new one comes into view.