Dynamic sizing for compass rose based on image size (#3826)

* Dynamic sizing for compass rose based on image size

- Compass rose now sizes and positions proportionally to the containing
image, with min and max sizes;
- Refactored computed `compassDimensionsStyle` as
`sizedImageDimensions` for reusability;
- Tweaked sizing of compass ordinals text and North arrow for better
legibility;
- Minor tweaks to element positioning and opacity for better legibility;
- TODO: add unit tests;

* Fix linting and code style

- Fixed lint errors;
- Better variable names;

* Address comments from PR #3826 review:

- Renamed `compassRoseSizing` to `compassRoseSizingClasses` and fixed
function logic;
- Fixed line breaks for code style;
This commit is contained in:
Charles Hacskaylo
2021-05-11 12:07:44 -07:00
committed by GitHub
parent 9920e67c83
commit 12416b8079
3 changed files with 363 additions and 289 deletions

View File

@ -23,7 +23,7 @@
<template> <template>
<div <div
class="c-compass" class="c-compass"
:style="compassDimensionsStyle" :style="`width: ${ sizedImageDimensions.width }px; height: ${ sizedImageDimensions.height }px`"
> >
<CompassHUD <CompassHUD
v-if="hasCameraFieldOfView" v-if="hasCameraFieldOfView"
@ -34,6 +34,7 @@
<CompassRose <CompassRose
v-if="hasCameraFieldOfView" v-if="hasCameraFieldOfView"
:heading="heading" :heading="heading"
:sized-image-width="sizedImageDimensions.width"
:sun-heading="sunHeading" :sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView" :camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan" :camera-pan="cameraPan"
@ -77,6 +78,20 @@ export default {
} }
}, },
computed: { computed: {
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.containerWidth / this.containerHeight) > this.naturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.containerHeight * this.naturalAspectRatio;
sizedImageDimensions.height = this.containerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.containerWidth;
sizedImageDimensions.height = this.containerWidth * this.naturalAspectRatio;
}
return sizedImageDimensions;
},
hasCameraFieldOfView() { hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0; return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
}, },
@ -94,25 +109,6 @@ export default {
}, },
cameraAngleOfView() { cameraAngleOfView() {
return CAMERA_ANGLE_OF_VIEW; return CAMERA_ANGLE_OF_VIEW;
},
compassDimensionsStyle() {
const containerAspectRatio = this.containerWidth / this.containerHeight;
let width;
let height;
if (containerAspectRatio < this.naturalAspectRatio) {
width = '100%';
height = `${ this.containerWidth / this.naturalAspectRatio }px`;
} else {
width = `${ this.containerHeight * this.naturalAspectRatio }px`;
height = '100%';
}
return {
width: width,
height: height
};
} }
}, },
methods: { methods: {

View File

@ -22,129 +22,134 @@
<template> <template>
<div <div
class="c-direction-rose" class="w-direction-rose"
@click="toggleLockCompass" :class="compassRoseSizingClasses"
> >
<div <div
class="c-nsew" class="c-direction-rose"
:style="compassRoseStyle" @click="toggleLockCompass"
> >
<svg <div
class="c-nsew__minor-ticks" class="c-nsew"
viewBox="0 0 100 100" :style="compassRoseStyle"
> >
<rect <svg
class="c-nsew__tick c-tick-ne" class="c-nsew__minor-ticks"
x="49" viewBox="0 0 100 100"
y="0" >
width="2" <rect
height="5" class="c-nsew__tick c-tick-ne"
/> x="49"
<rect y="0"
class="c-nsew__tick c-tick-se" width="2"
x="95" height="5"
y="49" />
width="5" <rect
height="2" class="c-nsew__tick c-tick-se"
/> x="95"
<rect y="49"
class="c-nsew__tick c-tick-sw" width="5"
x="49" height="2"
y="95" />
width="2" <rect
height="5" class="c-nsew__tick c-tick-sw"
/> x="49"
<rect y="95"
class="c-nsew__tick c-tick-nw" width="2"
x="0" height="5"
y="49" />
width="5" <rect
height="2" class="c-nsew__tick c-tick-nw"
/> x="0"
y="49"
width="5"
height="2"
/>
</svg> </svg>
<svg <svg
class="c-nsew__ticks" class="c-nsew__ticks"
viewBox="0 0 100 100" viewBox="0 0 100 100"
> >
<polygon <polygon
class="c-nsew__tick c-tick-n" class="c-nsew__tick c-tick-n"
points="50,0 57,5 43,5" points="50,0 60,10 40,10"
/> />
<rect <rect
class="c-nsew__tick c-tick-e" class="c-nsew__tick c-tick-e"
x="95" x="95"
y="49" y="49"
width="5" width="5"
height="2" height="2"
/> />
<rect <rect
class="c-nsew__tick c-tick-w" class="c-nsew__tick c-tick-w"
x="0" x="0"
y="49" y="49"
width="5" width="5"
height="2" height="2"
/> />
<rect <rect
class="c-nsew__tick c-tick-s" class="c-nsew__tick c-tick-s"
x="49" x="49"
y="95" y="95"
width="2" width="2"
height="5" height="5"
/> />
<text <text
class="c-nsew__label c-label-n" class="c-nsew__label c-label-n"
text-anchor="middle" text-anchor="middle"
:transform="northTextTransform" :transform="northTextTransform"
>N</text> >N</text>
<text <text
class="c-nsew__label c-label-e" class="c-nsew__label c-label-e"
text-anchor="middle" text-anchor="middle"
:transform="eastTextTransform" :transform="eastTextTransform"
>E</text> >E</text>
<text <text
class="c-nsew__label c-label-w" class="c-nsew__label c-label-w"
text-anchor="middle" text-anchor="middle"
:transform="southTextTransform" :transform="southTextTransform"
>W</text> >W</text>
<text <text
class="c-nsew__label c-label-s" class="c-nsew__label c-label-s"
text-anchor="middle" text-anchor="middle"
:transform="westTextTransform" :transform="westTextTransform"
>S</text> >S</text>
</svg> </svg>
</div>
<div
v-if="hasHeading"
class="c-spacecraft-body"
:style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div> </div>
<div class="cam-field-half cam-field-half-r">
<div <div
class="cam-field-area" v-if="hasHeading"
:style="cameraFOVStyleRightHalf" class="c-spacecraft-body"
></div> :style="headingStyle"
>
</div>
<div
v-if="hasSunHeading"
class="c-sun"
:style="sunHeadingStyle"
></div>
<div
class="c-cam-field"
:style="cameraPanStyle"
>
<div class="cam-field-half cam-field-half-l">
<div
class="cam-field-area"
:style="cameraFOVStyleLeftHalf"
></div>
</div>
<div class="cam-field-half cam-field-half-r">
<div
class="cam-field-area"
:style="cameraFOVStyleRightHalf"
></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -155,6 +160,10 @@ import { rotate } from './utils';
export default { export default {
props: { props: {
sizedImageWidth: {
type: Number,
required: true
},
heading: { heading: {
type: Number, type: Number,
required: true required: true
@ -177,12 +186,24 @@ export default {
} }
}, },
computed: { computed: {
north() { compassRoseSizingClasses() {
return this.lockCompass ? rotate(-this.cameraPan) : 0; let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
}, },
compassRoseStyle() { compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` }; return { transform: `rotate(${ this.north }deg)` };
}, },
north() {
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
northTextTransform() { northTextTransform() {
return this.cardinalPointsTextTransform.north; return this.cardinalPointsTextTransform.north;
}, },
@ -204,10 +225,10 @@ export default {
const rotation = `rotate(${ -this.north })`; const rotation = `rotate(${ -this.north })`;
return { return {
north: `translate(50,15) ${ rotation }`, north: `translate(50,23) ${ rotation }`,
east: `translate(87,50) ${ rotation }`, east: `translate(82,50) ${ rotation }`,
south: `translate(13,50) ${ rotation }`, south: `translate(18,50) ${ rotation }`,
west: `translate(50,87) ${ rotation }` west: `translate(50,82) ${ rotation }`
}; };
}, },
hasHeading() { hasHeading() {

View File

@ -20,195 +20,252 @@ $elemBg: rgba(black, 0.7);
/***************************** COMPASS HUD */ /***************************** COMPASS HUD */
.c-hud { .c-hud {
// To be placed within a imagery view, in the bounding box of the image // To be placed within a imagery view, in the bounding box of the image
$m: 1px; $m: 1px;
$padTB: 2px; $padTB: 2px;
$padLR: $padTB; $padLR: $padTB;
color: $interfaceKeyColor; color: $interfaceKeyColor;
font-size: 0.8em; font-size: 0.8em;
position: absolute;
top: $m; right: $m; left: $m;
height: 18px;
svg, div {
position: absolute; position: absolute;
} top: $m;
right: $m;
left: $m;
height: 18px;
&__display { svg, div {
height: 30px; position: absolute;
pointer-events: all; }
position: absolute;
top: 0;
right: 0;
left: 0;
}
&__range { &__display {
border: 1px solid $interfaceKeyColor; height: 30px;
border-top-color: transparent; pointer-events: all;
position: absolute; position: absolute;
top: 50%; right: $padLR; bottom: $padTB; left: $padLR; top: 0;
} right: 0;
left: 0;
}
[class*="__dir"] { &__range {
// NSEW border: 1px solid $interfaceKeyColor;
display: inline-block; border-top-color: transparent;
font-weight: bold; position: absolute;
text-shadow: 0 1px 2px black; top: 50%;
top: 50%; right: $padLR;
transform: translate(-50%,-50%); bottom: $padTB;
z-index: 2; left: $padLR;
} }
[class*="__dir--sub"] { [class*="__dir"] {
font-weight: normal; // NSEW
opacity: 0.5; display: inline-block;
} font-weight: bold;
text-shadow: 0 1px 2px black;
top: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
&__sun { [class*="__dir--sub"] {
$s: 10px; font-weight: normal;
@include sun('circle farthest-side at bottom'); opacity: 0.5;
bottom: $padTB + 2px; }
height: $s; width: $s*2;
opacity: 0.8; &__sun {
transform: translateX(-50%); $s: 10px;
z-index: 1; @include sun('circle farthest-side at bottom');
} bottom: $padTB + 2px;
height: $s;
width: $s*2;
opacity: 0.8;
transform: translateX(-50%);
z-index: 1;
}
} }
/***************************** COMPASS DIRECTIONS */ /***************************** COMPASS DIRECTIONS */
.c-nsew { .c-nsew {
$color: $interfaceKeyColor; $color: $interfaceKeyColor;
$inset: 7%; $inset: 5%;
$tickHeightPerc: 15%; $tickHeightPerc: 15%;
text-shadow: black 0 0 10px; text-shadow: black 0 0 10px;
top: $inset; right: $inset; bottom: $inset; left: $inset; top: $inset;
z-index: 3; right: $inset;
bottom: $inset;
left: $inset;
z-index: 3;
&__tick, &__tick,
&__label { &__label {
fill: $color; fill: $color;
} }
&__minor-ticks { &__minor-ticks {
opacity: 0.5; opacity: 0.5;
transform-origin: center; transform-origin: center;
transform: rotate(45deg); transform: rotate(45deg);
} }
&__label { &__label {
dominant-baseline: central; dominant-baseline: central;
font-size: 0.8em; font-size: 1.25em;
font-weight: bold; font-weight: bold;
} }
.c-label-n { .c-label-n {
font-size: 1.1em; font-size: 2em;
} }
} }
/***************************** CAMERA FIELD ANGLE */ /***************************** CAMERA FIELD ANGLE */
.c-cam-field { .c-cam-field {
$color: white; $color: white;
opacity: 0.2; opacity: 0.3;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
.cam-field-half {
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 2;
.cam-field-area { .cam-field-half {
background: $color; top: 0;
top: -30%; right: 0;
right: 0; bottom: 0;
bottom: -30%; left: 0;
left: 0;
}
// clip-paths overlap a bit to avoid a gap between halves .cam-field-area {
&-l { background: $color;
clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%); top: -30%;
.cam-field-area { right: 0;
transform-origin: left center; bottom: -30%;
} left: 0;
} }
&-r { // clip-paths overlap a bit to avoid a gap between halves
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%); &-l {
.cam-field-area { clip-path: polygon(0 0, 50.5% 0, 50.5% 100%, 0 100%);
transform-origin: right center;
} .cam-field-area {
transform-origin: left center;
}
}
&-r {
clip-path: polygon(49.5% 0, 100% 0, 100% 100%, 49.5% 100%);
.cam-field-area {
transform-origin: right center;
}
}
} }
}
} }
/***************************** SPACECRAFT BODY */ /***************************** SPACECRAFT BODY */
.c-spacecraft-body { .c-spacecraft-body {
$color: $interfaceKeyColor; $color: $interfaceKeyColor;
$s: 30%; $s: 30%;
background: $color;
border-radius: 3px;
height: $s; width: $s;
left: 50%; top: 50%;
opacity: 0.4;
transform-origin: center top;
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color; background: $color;
content: ''; border-radius: 3px;
display: block; height: $s;
position: absolute; width: $s;
top: 10%; right: 20%; bottom: 50%; left: 20%; left: 50%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY); top: 50%;
} opacity: 0.4;
transform-origin: center top;
transform: translateX(-50%); // center by default, overridden by CompassRose.vue / headingStyle()
&:before {
// Direction arrow
$color: rgba(black, 0.5);
$arwPointerY: 60%;
$arwBodyOffset: 25%;
background: $color;
content: '';
display: block;
position: absolute;
top: 10%;
right: 20%;
bottom: 50%;
left: 20%;
clip-path: polygon(50% 0, 100% $arwPointerY, 100%-$arwBodyOffset $arwPointerY, 100%-$arwBodyOffset 100%, $arwBodyOffset 100%, $arwBodyOffset $arwPointerY, 0 $arwPointerY);
}
} }
/***************************** DIRECTION ROSE */ /***************************** DIRECTION ROSE */
.c-direction-rose { .w-direction-rose {
$d: 100px; $s: 10%;
$c2: rgba(white, 0.1); $m: 2%;
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
width: $d;
height: $d;
transform-origin: 0 0;
position: absolute;
bottom: 10px; left: 10px;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
svg, div {
position: absolute; position: absolute;
} bottom: $m;
left: $m;
width: $s;
padding-top: $s;
// Sun &.--rose-min {
.c-sun { $s: 30px;
width: $s;
padding-top: $s;
}
&.--rose-small {
.c-nsew__minor-ticks,
.c-tick-w,
.c-tick-s,
.c-tick-e,
.c-label-w,
.c-label-s,
.c-label-e {
display: none;
}
.c-label-n {
font-size: 2.5em;
}
}
&.--rose-max {
$s: 100px;
width: $s;
padding-top: $s;
}
}
.c-direction-rose {
$c2: rgba(white, 0.1);
background: $elemBg;
background-image: radial-gradient(circle closest-side, transparent, transparent 80%, $c2);
transform-origin: 0 0;
position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
clip-path: circle(50% at 50% 50%);
border-radius: 100%;
&:before { svg, div {
$s: 35%; position: absolute;
@include sun(); }
content: '';
display: block; // Sun
position: absolute; .c-sun {
opacity: 0.7; top: 0;
top: 0; left: 50%; right: 0;
height:$s; width: $s; bottom: 0;
transform: translate(-50%, -60%); left: 0;
&:before {
$s: 35%;
@include sun();
content: '';
display: block;
position: absolute;
opacity: 0.7;
top: 0;
left: 50%;
height: $s;
width: $s;
transform: translate(-50%, -60%);
}
} }
}
} }