Render range-intervals using CSS polygons

This commit is contained in:
Rene Saarsoo 2020-11-22 23:10:12 +02:00
parent be0d236c68
commit 73d7852309
3 changed files with 85 additions and 12 deletions

View File

@ -1,7 +1,7 @@
import React, { useState, useCallback } from "react";
import { WorkoutPlot } from "./components/WorkoutPlot";
import { WorkoutStats } from "./components/WorkoutStats";
import { parse, chunkRangeIntervals, Duration } from "zwiftout";
import { parse } from "zwiftout";
import { ErrorMessage } from "./components/ErrorMessage";
import styled from "styled-components";
import { CodeEditor } from "./components/CodeEditor";
@ -29,9 +29,6 @@ const AppContainer = styled.div`
margin: 0 auto;
`;
// Split range-intervals into 1 minute chunks
const chunkSize = new Duration(60);
export function App() {
const [text, setText] = useState(defaultWorkout);
const [workout, setWorkout] = useState(parse(defaultWorkout));
@ -54,7 +51,7 @@ export function App() {
<AppContainer>
<AppTitle />
<CodeEditor onValueChange={onChange} value={text} />
<WorkoutPlot intervals={chunkRangeIntervals(workout.intervals, chunkSize)} />
<WorkoutPlot intervals={workout.intervals} />
{error && <ErrorMessage>{error}</ErrorMessage>}
<WorkoutStats workout={workout} />
<ZwoOutput workout={workout} />

View File

@ -1,3 +1,4 @@
import React from "react";
import styled from "styled-components";
import { ZoneType } from "zwiftout";
@ -30,3 +31,60 @@ export const Bar = styled.div<BarProps>`
background: ${(props) => zoneColorsMap[props.zone]};
transition: width 0.1s, height 0.1s, background-color 0.1s;
`;
export type RangeBarProps = {
startZone: ZoneType;
endZone: ZoneType;
// Percentage of total workout length
durationPercentage: number;
// Percentage of maximum intensity in workout
intensityStartPercentage: number;
intensityEndPercentage: number;
};
const createUpPolygon = (middle: number) => `polygon(0% 100%, 100% 100%, 100% 0%, 0% ${middle}%)`;
const createDownPolygon = (middle: number) => `polygon(0% 0%, 0% 100%, 100% 100%, 100% ${middle}%)`;
export const RangeContainer = styled.div<{
height: number;
width: number;
startZone: ZoneType;
endZone: ZoneType;
middle: number;
direction: "up" | "down";
}>`
display: inline-block;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
vertical-align: bottom;
margin-right: 0.1%;
/* exclude 0.1% margin from bar width */
width: ${(props) => props.width - 0.1}%;
height: ${(props) => props.height}%;
clip-path: ${({ direction, middle }) => (direction === "up" ? createUpPolygon(middle) : createDownPolygon(middle))};
background: linear-gradient(
to right,
${(props) => zoneColorsMap[props.startZone]},
${(props) => zoneColorsMap[props.endZone]}
);
transition: width 0.1s, height 0.1s, background-color 0.1s;
`;
export const RangeBar: React.FC<RangeBarProps> = (props) => {
const minHeightPercentage = Math.min(props.intensityStartPercentage, props.intensityEndPercentage);
const maxHeightPercentage = Math.max(props.intensityStartPercentage, props.intensityEndPercentage);
const bottomHeight = (minHeightPercentage / maxHeightPercentage) * 100;
const topHeight = 100 - bottomHeight;
const direction = props.intensityStartPercentage < props.intensityEndPercentage ? "up" : "down";
return (
<RangeContainer
height={maxHeightPercentage}
width={props.durationPercentage}
middle={topHeight}
direction={direction}
startZone={props.startZone}
endZone={props.endZone}
/>
);
};

View File

@ -1,7 +1,15 @@
import React from "react";
import styled from "styled-components";
import { Interval, Intensity, Duration, totalDuration, maximumIntensity } from "zwiftout";
import { BarProps, Bar } from "./Bar";
import {
Interval,
Intensity,
Duration,
totalDuration,
maximumIntensity,
RangeIntensity,
intensityValueToZoneType,
} from "zwiftout";
import { BarProps, Bar, RangeBar, RangeBarProps } from "./Bar";
const toBarProps = (interval: Interval, workoutDuration: Duration, maxIntensity: Intensity): BarProps => ({
zone: interval.intensity.zone,
@ -18,17 +26,27 @@ const Plot = styled.div`
margin: 10px 0;
`;
const toRangeBarProps = (interval: Interval, workoutDuration: Duration, maxIntensity: Intensity): RangeBarProps => ({
startZone: intensityValueToZoneType(interval.intensity.start),
endZone: intensityValueToZoneType(interval.intensity.end),
durationPercentage: (interval.duration.seconds / workoutDuration.seconds) * 100,
intensityStartPercentage: (interval.intensity.start / maxIntensity.value) * 100,
intensityEndPercentage: (interval.intensity.end / maxIntensity.value) * 100,
});
export const WorkoutPlot: React.FC<{ intervals: Interval[] }> = ({ intervals }) => {
const workoutDuration = totalDuration(intervals);
const maxIntensity = maximumIntensity(intervals);
return (
<Plot>
{intervals
.map((interval) => toBarProps(interval, workoutDuration, maxIntensity))
.map((props, i) => (
<Bar key={i} {...props} />
))}
{intervals.map((interval, i) => {
if (interval.intensity instanceof RangeIntensity) {
return <RangeBar key={i} {...toRangeBarProps(interval, workoutDuration, maxIntensity)} />;
} else {
return <Bar key={i} {...toBarProps(interval, workoutDuration, maxIntensity)} />;
}
})}
</Plot>
);
};