chore: rework ShipFit to use SVGs for better pixel-perfect rendering (#41)
This commit is contained in:
@@ -50,7 +50,7 @@ export const FitLink = () => {
|
||||
onClick: isRemoteViewer ? undefined : linkPropsClick,
|
||||
};
|
||||
|
||||
return <div className={styles.fitlink}>
|
||||
return <div className={styles.fitLink}>
|
||||
<svg viewBox="0 0 730 730" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="fitlink"
|
||||
|
||||
@@ -17,8 +17,6 @@ export const Hull = () => {
|
||||
}
|
||||
|
||||
return <div className={styles.hull}>
|
||||
<div className={styles.hullInner}>
|
||||
<img src={`https://images.evetech.net/types/${hull}/render?size=1024`} />
|
||||
</div>
|
||||
<img src={`https://images.evetech.net/types/${hull}/render?size=1024`} />
|
||||
</div>
|
||||
}
|
||||
|
||||
48
src/ShipFit/RadialMenu.tsx
Normal file
48
src/ShipFit/RadialMenu.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
const highlightSettings = {
|
||||
lowslot: {
|
||||
width: 12,
|
||||
height: 3,
|
||||
x: 0,
|
||||
y: 9,
|
||||
},
|
||||
medslot: {
|
||||
width: 3,
|
||||
height: 12,
|
||||
x: 9,
|
||||
y: 0,
|
||||
},
|
||||
hislot: {
|
||||
width: 12,
|
||||
height: 3,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const RadialMenu = (props: { type: "lowslot" | "medslot" | "hislot" }) => {
|
||||
const highlight = highlightSettings[props.type];
|
||||
|
||||
return <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" className={styles.radialMenu}>
|
||||
<defs>
|
||||
<mask id={`radial-menu-${props.type}`}>
|
||||
<rect style={{ fill: "#ffffff", fillOpacity: 0.5 }} width={12} height={12} x={0} y={0} />
|
||||
|
||||
<rect style={{ fill: "#ffffff" }} width={highlight.width} height={highlight.height} x={highlight.x} y={highlight.y} />
|
||||
|
||||
<circle style={{ fill: "#000000" }} cx={6} cy={6} r={5} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={3} x={0} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={3} x={9} y={0} />
|
||||
<rect style={{ fill: "#000000" }} width={3} height={4} x={0} y={9} />
|
||||
<rect style={{ fill: "#000000" }} width={4} height={4} x={9} y={9} />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<rect style={{ fill: "#ffffff" }} width={12} height={12} x={0} y={0} mask={`url(#radial-menu-${props.type})`} />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
23
src/ShipFit/RingInner.tsx
Normal file
23
src/ShipFit/RingInner.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingInner = () => {
|
||||
return <svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner}>
|
||||
<defs>
|
||||
<mask id="slot-corners">
|
||||
<rect style={{ fill: "#ffffff" }} width="512" height="512" x="0" y="0" />
|
||||
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="133" y="126" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="129" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="366" y="366" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="132" y="369" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="230" y="44" transform="rotate(56)" />
|
||||
</mask>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<circle style={{ fill: "none", stroke: "#000000", strokeWidth: 46, strokeOpacity: 0.6 }} cx="256" cy="256" r="195" mask="url(#slot-corners)" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
17
src/ShipFit/RingOuter.tsx
Normal file
17
src/ShipFit/RingOuter.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingOuter = () => {
|
||||
return <svg viewBox="24 24 464 464" xmlns="http://www.w3.org/2000/svg" className={styles.ringOuter}>
|
||||
<g>
|
||||
<circle style={{ fill: "none", stroke: "#000000", strokeWidth: 16 }} cx="256" cy="256" r="224" />
|
||||
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="98" y="89" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="401" y="93" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="402" y="401" />
|
||||
<rect style={{ fill: "#000000" }} width="17" height="17" x="94" y="402" />
|
||||
<rect style={{ fill: "#000000" }} width="12" height="12" x="196" y="82" transform="rotate(56)" />
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
19
src/ShipFit/RingTop.tsx
Normal file
19
src/ShipFit/RingTop.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const RingTop = (props: { children: React.ReactNode }) => {
|
||||
return <div className={styles.ringTop}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const RingTopItem = (props: { children: React.ReactNode, rotation: number }) => {
|
||||
const rotationStyle = {
|
||||
"--rotation": `${props.rotation}deg`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return <div className={styles.ringTopItem} style={rotationStyle}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
@@ -1,121 +1,24 @@
|
||||
|
||||
.fit {
|
||||
border-radius: 50%;
|
||||
border: 1px solid black;
|
||||
height: calc(var(--radius) * 2);
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: calc(var(--radius) * 2);
|
||||
|
||||
--radius-slots: calc(var(--radius) * 0.92);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.outerBand {
|
||||
border-radius: 50%;
|
||||
border: calc((var(--radius) - var(--radius-slots)) * 0.8) solid black;
|
||||
box-sizing: border-box;
|
||||
height: calc(var(--radius) * 2);
|
||||
.ringOuter {
|
||||
filter: drop-shadow(0 0 6px #000000);
|
||||
position: absolute;
|
||||
width: calc(var(--radius) * 2);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.innerBand {
|
||||
border-radius: 50%;
|
||||
border: calc(var(--radius-slots) / 6 + var(--radius) - var(--radius-slots)) solid black;
|
||||
box-sizing: border-box;
|
||||
height: calc(var(--radius) * 2);
|
||||
opacity: 0.5;
|
||||
.ringInner {
|
||||
position: absolute;
|
||||
width: calc(var(--radius) * 2);
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hull {
|
||||
height: 1px;
|
||||
left: var(--radius);
|
||||
position: absolute;
|
||||
top: var(--radius);
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.hullInner {
|
||||
margin-left: calc(var(--radius) * -1);
|
||||
margin-top: calc(var(--radius) * -1);
|
||||
}
|
||||
.hullInner > img {
|
||||
border-radius: 50%;
|
||||
height: calc(var(--radius) * 2);
|
||||
width: calc(var(--radius) * 2);
|
||||
}
|
||||
|
||||
.slots {
|
||||
margin-left: calc(var(--radius) - var(--radius-slots));
|
||||
margin-top: calc(var(--radius) - var(--radius-slots));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slot {
|
||||
height: 1px;
|
||||
left: var(--radius-slots);
|
||||
position: absolute;
|
||||
transform-origin: 0 var(--radius-slots);
|
||||
transform: rotate(var(--rotation));
|
||||
width: 1px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.slotInner {
|
||||
border-top-left-radius: 100% 4px;
|
||||
border-top-right-radius: 100% 4px;
|
||||
border: 1px solid black;
|
||||
box-sizing: border-box;
|
||||
height: calc(var(--radius-slots) / 7);
|
||||
left: calc(var(--radius-slots) / 7 / 2 * -1);
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
transform: perspective(8px) rotateX(-1.5deg);
|
||||
width: calc(var(--radius-slots) / 7);
|
||||
}
|
||||
|
||||
.slotInnerInvalid {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
.slotInner[data-state="Passive"] {
|
||||
background-color: #0000002d;
|
||||
border-color: #6c6c6c9f;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.slotInner[data-state="Online"] {
|
||||
background-color: #9595952d;
|
||||
border-color: #9595959f;
|
||||
}
|
||||
.slotInner[data-state="Active"] {
|
||||
background-color: #87d3282d;
|
||||
border-color: #87d3289f;
|
||||
}
|
||||
.slotInner[data-state="Overload"] {
|
||||
background-color: #e8342b2d;
|
||||
border-color: #e8342b9f;
|
||||
}
|
||||
|
||||
.slotItem {
|
||||
--reverse-rotation: calc(-1 * var(--rotation));
|
||||
left: -32px;
|
||||
position: absolute;
|
||||
top: calc(-32px + var(--radius-slots) / 7 / 2);
|
||||
transform: rotate(var(--reverse-rotation)) scale(calc(var(--scale) * 0.8));
|
||||
}
|
||||
|
||||
.slotItemOffline {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.slotItem > img {
|
||||
border-top-left-radius: 32px;
|
||||
}
|
||||
|
||||
.fitlink {
|
||||
.fitLink {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
@@ -123,3 +26,99 @@
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.hull {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hull > img {
|
||||
border-radius: 50%;
|
||||
height: calc(100% - 2 * 3%);
|
||||
left: 3%;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
top: 3%;
|
||||
width: calc(100% - 2 * 3%);
|
||||
}
|
||||
|
||||
.ringTop {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ringTopItem {
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: rotate(var(--rotation));
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.ringTopItem > div, .ringTopItem > svg {
|
||||
--reverse-rotation: calc(-1 * var(--rotation));
|
||||
left: 50%;
|
||||
pointer-events: all;
|
||||
position: absolute;
|
||||
top: 3.5%;
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
}
|
||||
|
||||
.radialMenu {
|
||||
filter: drop-shadow(0 0 2px #ffffff);
|
||||
position: absolute;
|
||||
margin-top: 3.5%;
|
||||
width: 2.5%;
|
||||
}
|
||||
|
||||
.slot {
|
||||
height: 9.5%;
|
||||
margin-left: -2.5%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
.slot > svg {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transform: rotate(var(--rotation));
|
||||
width: 100%;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.slotImage {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
margin-top: 8%;
|
||||
margin-left: -10%;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.slotImage > img {
|
||||
border-top-left-radius: 50%;
|
||||
width: 120%;
|
||||
}
|
||||
|
||||
.slot > svg {
|
||||
fill: #999999;
|
||||
stroke: #999999;
|
||||
}
|
||||
.slot[data-state="Active"] > svg {
|
||||
fill: #8ae04a;
|
||||
stroke: #8ae04a;
|
||||
}
|
||||
.slot[data-state="Overload"] > svg {
|
||||
fill: #fd2d2d;
|
||||
stroke: #fd2d2d;
|
||||
}
|
||||
.slot[data-state="Offline"] {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.slot[data-state="Unavailable"] {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ const meta: Meta<typeof ShipFit> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ShipFit>;
|
||||
|
||||
const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => {
|
||||
const withShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<Story />
|
||||
<div style={{ width: context.args.width, height: context.args.width }}>
|
||||
<Story />
|
||||
</div>
|
||||
</ShipSnapshotProvider>
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
@@ -31,7 +33,7 @@ const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
radius: 365,
|
||||
width: 730,
|
||||
},
|
||||
decorators: [withShipSnapshotProvider],
|
||||
parameters: {
|
||||
|
||||
@@ -5,70 +5,69 @@ import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
import { FitLink } from './FitLink';
|
||||
import { Hull } from './Hull';
|
||||
import { Slot } from './Slot';
|
||||
import { RadialMenu } from "./RadialMenu";
|
||||
import { RingOuter } from "./RingOuter";
|
||||
import { RingInner } from "./RingInner";
|
||||
import { RingTop, RingTopItem } from "./RingTop";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export interface ShipFitProps {
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a ship fit similar to how it is done in-game.
|
||||
*/
|
||||
export const ShipFit = (props: ShipFitProps) => {
|
||||
const radius = props.radius ?? 365;
|
||||
|
||||
export const ShipFit = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
const slots = shipSnapshot.slots;
|
||||
|
||||
const scaleStyle = {
|
||||
"--radius": `${radius}px`,
|
||||
"--scale": `${radius / 365}`
|
||||
} as React.CSSProperties;
|
||||
|
||||
return <div className={styles.fit} style={scaleStyle}>
|
||||
<div className={styles.outerBand} />
|
||||
<div className={styles.innerBand} />
|
||||
return <div className={styles.fit}>
|
||||
<RingOuter />
|
||||
<RingInner />
|
||||
|
||||
<Hull />
|
||||
<FitLink />
|
||||
|
||||
<div className={styles.slots}>
|
||||
<Slot type="subsystem" index={1} fittable={slots?.subsystem >= 1} rotation="-125deg" />
|
||||
<Slot type="subsystem" index={2} fittable={slots?.subsystem >= 2} rotation="-114deg" />
|
||||
<Slot type="subsystem" index={3} fittable={slots?.subsystem >= 3} rotation="-103deg" />
|
||||
<Slot type="subsystem" index={4} fittable={slots?.subsystem >= 4} rotation="-92deg" />
|
||||
<RingTop>
|
||||
<RingTopItem rotation={-45}><RadialMenu type="hislot" /></RingTopItem>
|
||||
|
||||
<Slot type="rig" index={1} fittable={slots?.rig >= 1} rotation="-73deg" />
|
||||
<Slot type="rig" index={2} fittable={slots?.rig >= 2} rotation="-63deg" />
|
||||
<Slot type="rig" index={3} fittable={slots?.rig >= 3} rotation="-53deg" />
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 0}><Slot type="hislot" index={1} fittable={slots?.hislot >= 1} main /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 1}><Slot type="hislot" index={2} fittable={slots?.hislot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 2}><Slot type="hislot" index={3} fittable={slots?.hislot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 3}><Slot type="hislot" index={4} fittable={slots?.hislot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 4}><Slot type="hislot" index={5} fittable={slots?.hislot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 5}><Slot type="hislot" index={6} fittable={slots?.hislot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 6}><Slot type="hislot" index={7} fittable={slots?.hislot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={-36.5 + 71 / 7 * 7}><Slot type="hislot" index={8} fittable={slots?.hislot >= 8} /></RingTopItem>
|
||||
|
||||
<Slot type="hislot" index={1} fittable={slots?.hislot >= 1} rotation="-34deg" />
|
||||
<Slot type="hislot" index={2} fittable={slots?.hislot >= 2} rotation="-24deg" />
|
||||
<Slot type="hislot" index={3} fittable={slots?.hislot >= 3} rotation="-14deg" />
|
||||
<Slot type="hislot" index={4} fittable={slots?.hislot >= 4} rotation="-4deg" />
|
||||
<Slot type="hislot" index={5} fittable={slots?.hislot >= 5} rotation="6deg" />
|
||||
<Slot type="hislot" index={6} fittable={slots?.hislot >= 6} rotation="16deg" />
|
||||
<Slot type="hislot" index={7} fittable={slots?.hislot >= 7} rotation="26deg" />
|
||||
<Slot type="hislot" index={8} fittable={slots?.hislot >= 8} rotation="36deg" />
|
||||
<RingTopItem rotation={43}><RadialMenu type="medslot" /></RingTopItem>
|
||||
|
||||
<Slot type="medslot" index={1} fittable={slots?.medslot >= 1} rotation="55deg" />
|
||||
<Slot type="medslot" index={2} fittable={slots?.medslot >= 2} rotation="65deg" />
|
||||
<Slot type="medslot" index={3} fittable={slots?.medslot >= 3} rotation="75deg" />
|
||||
<Slot type="medslot" index={4} fittable={slots?.medslot >= 4} rotation="85deg" />
|
||||
<Slot type="medslot" index={5} fittable={slots?.medslot >= 5} rotation="95deg" />
|
||||
<Slot type="medslot" index={6} fittable={slots?.medslot >= 6} rotation="105deg" />
|
||||
<Slot type="medslot" index={7} fittable={slots?.medslot >= 7} rotation="115deg" />
|
||||
<Slot type="medslot" index={8} fittable={slots?.medslot >= 8} rotation="125deg" />
|
||||
<RingTopItem rotation={53 + 72 / 7 * 0}><Slot type="medslot" index={1} fittable={slots?.medslot >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 1}><Slot type="medslot" index={2} fittable={slots?.medslot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 2}><Slot type="medslot" index={3} fittable={slots?.medslot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 3}><Slot type="medslot" index={4} fittable={slots?.medslot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 4}><Slot type="medslot" index={5} fittable={slots?.medslot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 5}><Slot type="medslot" index={6} fittable={slots?.medslot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 6}><Slot type="medslot" index={7} fittable={slots?.medslot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={53 + 72 / 7 * 7}><Slot type="medslot" index={8} fittable={slots?.medslot >= 8} /></RingTopItem>
|
||||
|
||||
<Slot type="lowslot" index={1} fittable={slots?.lowslot >= 1} rotation="144deg" />
|
||||
<Slot type="lowslot" index={2} fittable={slots?.lowslot >= 2} rotation="154deg" />
|
||||
<Slot type="lowslot" index={3} fittable={slots?.lowslot >= 3} rotation="164deg" />
|
||||
<Slot type="lowslot" index={4} fittable={slots?.lowslot >= 4} rotation="174deg" />
|
||||
<Slot type="lowslot" index={5} fittable={slots?.lowslot >= 5} rotation="184deg" />
|
||||
<Slot type="lowslot" index={6} fittable={slots?.lowslot >= 6} rotation="194deg" />
|
||||
<Slot type="lowslot" index={7} fittable={slots?.lowslot >= 7} rotation="204deg" />
|
||||
<Slot type="lowslot" index={8} fittable={slots?.lowslot >= 8} rotation="214deg" />
|
||||
</div>
|
||||
<RingTopItem rotation={133}><RadialMenu type="lowslot" /></RingTopItem>
|
||||
|
||||
<RingTopItem rotation={142 + 72 / 7 * 0}><Slot type="lowslot" index={1} fittable={slots?.lowslot >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 1}><Slot type="lowslot" index={2} fittable={slots?.lowslot >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 2}><Slot type="lowslot" index={3} fittable={slots?.lowslot >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 3}><Slot type="lowslot" index={4} fittable={slots?.lowslot >= 4} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 4}><Slot type="lowslot" index={5} fittable={slots?.lowslot >= 5} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 5}><Slot type="lowslot" index={6} fittable={slots?.lowslot >= 6} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 6}><Slot type="lowslot" index={7} fittable={slots?.lowslot >= 7} /></RingTopItem>
|
||||
<RingTopItem rotation={142 + 72 / 7 * 7}><Slot type="lowslot" index={8} fittable={slots?.lowslot >= 8} /></RingTopItem>
|
||||
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 0}><Slot type="rig" index={1} fittable={slots?.rig >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 1}><Slot type="rig" index={2} fittable={slots?.rig >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-74 + 21 / 2 * 2}><Slot type="rig" index={3} fittable={slots?.rig >= 3} /></RingTopItem>
|
||||
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 0}><Slot type="subsystem" index={1} fittable={slots?.subsystem >= 1} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 1}><Slot type="subsystem" index={2} fittable={slots?.subsystem >= 2} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 2}><Slot type="subsystem" index={3} fittable={slots?.subsystem >= 3} /></RingTopItem>
|
||||
<RingTopItem rotation={-128 + 38 / 3 * 3}><Slot type="subsystem" index={4} fittable={slots?.subsystem >= 4} /></RingTopItem>
|
||||
</RingTop>
|
||||
</div>
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import ctlx from "clsx";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
@@ -31,26 +30,56 @@ const stateRotation: Record<string, string[]> = {
|
||||
"Overload": ["Passive", "Online", "Active", "Overload"],
|
||||
};
|
||||
|
||||
export const Slot = (props: {type: string, index: number, fittable: boolean, rotation: string}) => {
|
||||
export const Slot = (props: { type: string, index: number, fittable: boolean, main?: boolean }) => {
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const rotationStyle = { "--rotation": props.rotation } as React.CSSProperties;
|
||||
const esiFlag = esiFlagMapping[props.type][props.index - 1];
|
||||
|
||||
const esiItem = shipSnapshot?.items?.find((item) => item.flag == esiFlag);
|
||||
const active = esiItem?.max_state !== "Passive" && esiItem?.max_state !== "Online";
|
||||
|
||||
let item = <></>;
|
||||
let svg = <></>;
|
||||
|
||||
if (props.main !== undefined) {
|
||||
svg = <svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin slice" style={{ display: "none" }}>
|
||||
<g id="slot">
|
||||
<path style={{ fillOpacity: 0.1, strokeWidth: 1, strokeOpacity: 0.5 }} d="M 243 46 A 210 210 0 0 1 279 46.7 L 276 84.7 A 172 172 0 0 0 246 84 L 243 46" />
|
||||
</g>
|
||||
<g id="slot-active">
|
||||
<path style={{ fillOpacity: 0.6, strokeWidth: 1 }} d="M 250 84 L 254 79 L 268 79 L 272 84" />
|
||||
</g>
|
||||
<g id="slot-passive">
|
||||
<path style={{ strokeWidth: 1 }} d="M 245 48 A 208 208 0 0 1 250 47.5 L 248 50 L 245 50" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 277.5 48.5 A 208 208 0 0 0 273 48 L 275 50.5 L 277.5 50.5" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 247 82 A 170 170 0 0 1 252 82 L 250 80 L 246.8 80" />
|
||||
<path style={{ strokeWidth: 1 }} d="M 275 82.5 A 170 170 0 0 0 270 82 L 272 80 L 275.2 80" />
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
svg = <>
|
||||
{svg}
|
||||
<svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner} preserveAspectRatio="xMidYMin slice">
|
||||
<use href="#slot" />
|
||||
{props.fittable && active && <use href="#slot-active" />}
|
||||
{props.fittable && !active && <use href="#slot-passive" />}
|
||||
</svg>
|
||||
</>;
|
||||
|
||||
/* Not fittable and nothing fitted; no need to render the slot. */
|
||||
if (esiItem === undefined && !props.fittable) {
|
||||
return <></>
|
||||
return <div className={styles.slot} data-state="Unavailable">
|
||||
{svg}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (esiItem !== undefined) {
|
||||
item = <img src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`} title={eveData?.typeIDs?.[esiItem.type_id].name} />
|
||||
}
|
||||
|
||||
const isOffline = esiItem?.state === "Passive" && esiItem?.max_state !== "Passive";
|
||||
const state = (esiItem?.state === "Passive" && esiItem?.max_state !== "Passive") ? "Offline" : esiItem?.state;
|
||||
|
||||
function cycleState(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
if (!shipSnapshot?.loaded || !esiItem) return;
|
||||
@@ -68,10 +97,9 @@ export const Slot = (props: {type: string, index: number, fittable: boolean, rot
|
||||
shipSnapshot.setItemState(esiItem.flag, newState);
|
||||
}
|
||||
|
||||
return <div className={styles.slot} style={rotationStyle} onClick={cycleState}>
|
||||
<div className={ctlx(styles.slotInner, { [styles.slotInnerInvalid]: !props.fittable, })} data-state={esiItem?.state}>
|
||||
</div>
|
||||
<div className={ctlx(styles.slotItem, { [styles.slotItemOffline]: isOffline })}>
|
||||
return <div className={styles.slot} onClick={cycleState} data-state={state}>
|
||||
{svg}
|
||||
<div className={styles.slotImage}>
|
||||
{item}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
background-color: #111111;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
padding-bottom: 60px;
|
||||
padding-left: 50px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: calc(var(--radius) * 2 + 2 * 50px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cpuPg {
|
||||
|
||||
@@ -17,12 +17,14 @@ const meta: Meta<typeof ShipFitExtended> = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ShipFitExtended>;
|
||||
|
||||
const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => {
|
||||
const withShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<Story />
|
||||
<div style={{ width: context.args.width, height: context.args.width }}>
|
||||
<Story />
|
||||
</div>
|
||||
</ShipSnapshotProvider>
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
@@ -31,7 +33,7 @@ const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
radius: 365,
|
||||
width: 730,
|
||||
},
|
||||
decorators: [withShipSnapshotProvider],
|
||||
parameters: {
|
||||
|
||||
@@ -5,10 +5,6 @@ import { ShipAttribute } from "../ShipAttribute";
|
||||
|
||||
import styles from "./ShipFitExtended.module.css";
|
||||
|
||||
export interface ShipFitExtendedProps {
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
const CargoHold = () => {
|
||||
return <div>
|
||||
<div className={styles.cargoIcon}>
|
||||
@@ -61,15 +57,9 @@ const CpuPg = (props: { title: string, children: React.ReactNode }) => {
|
||||
* also adds the cargo hold, drone bay, and CPU/PG usage at the
|
||||
* bottom of the fit.
|
||||
*/
|
||||
export const ShipFitExtended = (props: ShipFitExtendedProps) => {
|
||||
const radius = props.radius ?? 365;
|
||||
|
||||
const scaleStyle = {
|
||||
"--radius": `${radius}px`,
|
||||
} as React.CSSProperties;
|
||||
|
||||
return <div className={styles.fit} style={scaleStyle}>
|
||||
<ShipFit radius={radius} />
|
||||
export const ShipFitExtended = () => {
|
||||
return <div className={styles.fit}>
|
||||
<ShipFit />
|
||||
|
||||
<div className={styles.cargoHold}>
|
||||
<CargoHold />
|
||||
|
||||
@@ -21,8 +21,8 @@ export interface ShipSnapshotItem {
|
||||
type_id: number,
|
||||
quantity: number,
|
||||
flag: number,
|
||||
state: string,
|
||||
max_state: string,
|
||||
state: "Passive" | "Online" | "Active" | "Overload",
|
||||
max_state: "Passive" | "Online" | "Active" | "Overload",
|
||||
attributes: Map<number, ShipSnapshotItemAttribute>,
|
||||
effects: number[],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user