Skip to content

Commit

Permalink
chore: rework ShipFit to use SVGs for better pixel-perfect rendering (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
TrueBrain authored Dec 10, 2023
1 parent e60aa71 commit aa76928
Show file tree
Hide file tree
Showing 14 changed files with 291 additions and 167 deletions.
2 changes: 1 addition & 1 deletion src/ShipFit/FitLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 1 addition & 3 deletions src/ShipFit/Hull.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 48 additions & 0 deletions src/ShipFit/RadialMenu.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions src/ShipFit/RingInner.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 17 additions & 0 deletions src/ShipFit/RingOuter.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions src/ShipFit/RingTop.tsx
Original file line number Diff line number Diff line change
@@ -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>
}
163 changes: 81 additions & 82 deletions src/ShipFit/ShipFit.module.css
Original file line number Diff line number Diff line change
@@ -1,125 +1,124 @@

.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;
}
.ringInner {
position: absolute;
width: 100%;
z-index: 1;
}

.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;
.fitLink {
height: 100%;
left: 0;
position: absolute;
width: calc(var(--radius) * 2);
top: 0;
width: 100%;
z-index: 3;
}

.hull {
height: 1px;
left: var(--radius);
height: 100%;
position: absolute;
top: var(--radius);
width: 1px;
width: 100%;
}

.hullInner {
margin-left: calc(var(--radius) * -1);
margin-top: calc(var(--radius) * -1);
}
.hullInner > img {
.hull > img {
border-radius: 50%;
height: calc(var(--radius) * 2);
width: calc(var(--radius) * 2);
height: calc(100% - 2 * 3%);
left: 3%;
opacity: 0.8;
position: relative;
top: 3%;
width: calc(100% - 2 * 3%);
}

.slots {
margin-left: calc(var(--radius) - var(--radius-slots));
margin-top: calc(var(--radius) - var(--radius-slots));
position: relative;
.ringTop {
height: 100%;
position: absolute;
width: 100%;
}

.slot {
height: 1px;
left: var(--radius-slots);
.ringTopItem {
height: 100%;
pointer-events: none;
position: absolute;
transform-origin: 0 var(--radius-slots);
transform: rotate(var(--rotation));
width: 1px;
width: 100%;
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);
.ringTopItem > div, .ringTopItem > svg {
--reverse-rotation: calc(-1 * var(--rotation));
left: 50%;
pointer-events: all;
position: absolute;
top: 2px;
transform: perspective(8px) rotateX(-1.5deg);
width: calc(var(--radius-slots) / 7);
top: 3.5%;
transform: rotate(var(--reverse-rotation));
}

.slotInnerInvalid {
border: 1px solid red;
.radialMenu {
filter: drop-shadow(0 0 2px #ffffff);
position: absolute;
margin-top: 3.5%;
width: 2.5%;
}

.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;
.slot {
height: 9.5%;
margin-left: -2.5%;
position: relative;
user-select: none;
width: 7%;
}

.slotItem {
--reverse-rotation: calc(-1 * var(--rotation));
left: -32px;
.slot > svg {
height: 100%;
position: absolute;
top: calc(-32px + var(--radius-slots) / 7 / 2);
transform: rotate(var(--reverse-rotation)) scale(calc(var(--scale) * 0.8));
transform: rotate(var(--rotation));
width: 100%;
z-index: 4;
}

.slotItemOffline {
opacity: 0.3;
.slotImage {
height: 100%;
position: absolute;
margin-top: 8%;
margin-left: -10%;
width: 100%;
z-index: 5;
}

.slotItem > img {
border-top-left-radius: 32px;
.slotImage > img {
border-top-left-radius: 50%;
width: 120%;
}

.fitlink {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 3;
.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;
}
8 changes: 5 additions & 3 deletions src/ShipFit/ShipFit.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -31,7 +33,7 @@ const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context

export const Default: Story = {
args: {
radius: 365,
width: 730,
},
decorators: [withShipSnapshotProvider],
parameters: {
Expand Down
Loading

0 comments on commit aa76928

Please sign in to comment.