Skip to content

Commit 45e8f98

Browse files
committed
Added aspect ratio calculator
1 parent f1a5489 commit 45e8f98

File tree

6 files changed

+331
-1
lines changed

6 files changed

+331
-1
lines changed

components.d.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ declare module '@vue/runtime-core' {
1313
About: typeof import('./src/pages/About.vue')['default']
1414
App: typeof import('./src/App.vue')['default']
1515
AsciiTextDrawer: typeof import('./src/tools/ascii-text-drawer/ascii-text-drawer.vue')['default']
16+
AspectRatioCalculator: typeof import('./src/tools/aspect-ratio-calculator/aspect-ratio-calculator.vue')['default']
1617
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
1718
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
1819
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
@@ -89,17 +90,28 @@ declare module '@vue/runtime-core' {
8990
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
9091
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
9192
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
93+
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
9294
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
95+
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
96+
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
97+
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
9398
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
9499
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
95100
IconMdiClose: typeof import('~icons/mdi/close')['default']
96101
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
102+
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
103+
IconMdiDownload: typeof import('~icons/mdi/download')['default']
97104
IconMdiEye: typeof import('~icons/mdi/eye')['default']
98105
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
99106
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
107+
IconMdiPause: typeof import('~icons/mdi/pause')['default']
108+
IconMdiPlay: typeof import('~icons/mdi/play')['default']
109+
IconMdiRecord: typeof import('~icons/mdi/record')['default']
110+
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
100111
IconMdiSearch: typeof import('~icons/mdi/search')['default']
101112
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
102113
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
114+
IconMdiVideo: typeof import('~icons/mdi/video')['default']
103115
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
104116
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
105117
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
@@ -127,18 +139,40 @@ declare module '@vue/runtime-core' {
127139
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
128140
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
129141
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
142+
NAlert: typeof import('naive-ui')['NAlert']
130143
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
144+
NCheckbox: typeof import('naive-ui')['NCheckbox']
131145
NCode: typeof import('naive-ui')['NCode']
132146
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
147+
NColorPicker: typeof import('naive-ui')['NColorPicker']
133148
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
149+
NDatePicker: typeof import('naive-ui')['NDatePicker']
150+
NDivider: typeof import('naive-ui')['NDivider']
151+
NDynamicInput: typeof import('naive-ui')['NDynamicInput']
134152
NEllipsis: typeof import('naive-ui')['NEllipsis']
153+
NForm: typeof import('naive-ui')['NForm']
154+
NFormItem: typeof import('naive-ui')['NFormItem']
155+
NGi: typeof import('naive-ui')['NGi']
156+
NGrid: typeof import('naive-ui')['NGrid']
135157
NH1: typeof import('naive-ui')['NH1']
158+
NH2: typeof import('naive-ui')['NH2']
136159
NH3: typeof import('naive-ui')['NH3']
137160
NIcon: typeof import('naive-ui')['NIcon']
161+
NImage: typeof import('naive-ui')['NImage']
162+
NInputGroup: typeof import('naive-ui')['NInputGroup']
163+
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
164+
NInputNumber: typeof import('naive-ui')['NInputNumber']
138165
NLayout: typeof import('naive-ui')['NLayout']
139166
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
140167
NMenu: typeof import('naive-ui')['NMenu']
168+
NProgress: typeof import('naive-ui')['NProgress']
141169
NScrollbar: typeof import('naive-ui')['NScrollbar']
170+
NSlider: typeof import('naive-ui')['NSlider']
171+
NSpin: typeof import('naive-ui')['NSpin']
172+
NStatistic: typeof import('naive-ui')['NStatistic']
173+
NSwitch: typeof import('naive-ui')['NSwitch']
174+
NTable: typeof import('naive-ui')['NTable']
175+
NTag: typeof import('naive-ui')['NTag']
142176
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
143177
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
144178
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// aspect-ratio-calculator.service.test.ts
2+
3+
import { describe, expect, it } from 'vitest';
4+
import {
5+
type AspectRatio,
6+
type Dimensions,
7+
calculateAspectRatio,
8+
calculateDimensions,
9+
simplifyRatio,
10+
} from './aspect-ratio-calculator.service';
11+
12+
describe('Aspect Ratio Calculator Service', () => {
13+
describe('calculateAspectRatio', () => {
14+
it('calculates correct aspect ratio for 1920x1080', () => {
15+
const result = calculateAspectRatio(1920, 1080);
16+
expect(result).toEqual({ r1: 16, r2: 9 });
17+
});
18+
19+
it('calculates correct aspect ratio for 640x480', () => {
20+
const result = calculateAspectRatio(640, 480);
21+
expect(result).toEqual({ r1: 4, r2: 3 });
22+
});
23+
24+
it('handles square aspect ratio', () => {
25+
const result = calculateAspectRatio(1000, 1000);
26+
expect(result).toEqual({ r1: 1, r2: 1 });
27+
});
28+
});
29+
30+
describe('calculateDimensions', () => {
31+
it('calculates correct height given width and 16:9 ratio', () => {
32+
const ratio: AspectRatio = { r1: 16, r2: 9 };
33+
const result = calculateDimensions(1920, ratio, true);
34+
expect(result).toEqual({ width: 1920, height: 1080 });
35+
});
36+
37+
it('calculates correct width given height and 4:3 ratio', () => {
38+
const ratio: AspectRatio = { r1: 4, r2: 3 };
39+
const result = calculateDimensions(480, ratio, false);
40+
expect(result).toEqual({ width: 640, height: 480 });
41+
});
42+
43+
it('handles 1:1 ratio', () => {
44+
const ratio: AspectRatio = { r1: 1, r2: 1 };
45+
const result = calculateDimensions(500, ratio, true);
46+
expect(result).toEqual({ width: 500, height: 500 });
47+
});
48+
});
49+
50+
describe('simplifyRatio', () => {
51+
it('simplifies 16:9 ratio', () => {
52+
const result = simplifyRatio(16, 9);
53+
expect(result).toEqual({ r1: 16, r2: 9 });
54+
});
55+
56+
it('simplifies 1920:1080 to 16:9', () => {
57+
const result = simplifyRatio(1920, 1080);
58+
expect(result).toEqual({ r1: 16, r2: 9 });
59+
});
60+
61+
it('simplifies 4:2 to 2:1', () => {
62+
const result = simplifyRatio(4, 2);
63+
expect(result).toEqual({ r1: 2, r2: 1 });
64+
});
65+
66+
it('handles already simplified ratios', () => {
67+
const result = simplifyRatio(7, 5);
68+
expect(result).toEqual({ r1: 7, r2: 5 });
69+
});
70+
});
71+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// aspect-ratio-calculator.service.ts
2+
3+
export interface AspectRatio {
4+
r1: number
5+
r2: number
6+
}
7+
8+
export interface Dimensions {
9+
width: number
10+
height: number
11+
}
12+
13+
export function calculateAspectRatio(width: number, height: number): AspectRatio {
14+
const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
15+
const divisor = gcd(width, height);
16+
return {
17+
r1: width / divisor,
18+
r2: height / divisor,
19+
};
20+
}
21+
22+
export function calculateDimensions(
23+
knownDimension: number,
24+
ratio: AspectRatio,
25+
isWidth: boolean,
26+
): Dimensions {
27+
if (isWidth) {
28+
const height = Math.round((knownDimension * ratio.r2) / ratio.r1);
29+
return { width: knownDimension, height };
30+
}
31+
else {
32+
const width = Math.round((knownDimension * ratio.r1) / ratio.r2);
33+
return { width, height: knownDimension };
34+
}
35+
}
36+
37+
export function simplifyRatio(r1: number, r2: number): AspectRatio {
38+
const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b));
39+
const divisor = gcd(r1, r2);
40+
return {
41+
r1: r1 / divisor,
42+
r2: r2 / divisor,
43+
};
44+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!-- AspectRatioCalculator.vue -->
2+
<script setup lang="ts">
3+
import { computed, ref, watch } from 'vue';
4+
import { NDivider, NInputNumber, NSelect, NSpace } from 'naive-ui';
5+
import {
6+
type AspectRatio,
7+
calculateAspectRatio,
8+
calculateDimensions,
9+
simplifyRatio,
10+
} from './aspect-ratio-calculator.service';
11+
12+
const width = ref<number | null>(null);
13+
const height = ref<number | null>(null);
14+
const r1 = ref<number | null>(null);
15+
const r2 = ref<number | null>(null);
16+
17+
const presets = [
18+
{ label: 'HD Video 16:9', value: '16:9' },
19+
{ label: 'SD Video 4:3', value: '4:3' },
20+
{ label: 'Widescreen 21:9', value: '21:9' },
21+
{ label: 'Square 1:1', value: '1:1' },
22+
];
23+
const selectedPreset = ref(null);
24+
25+
const aspectRatio = computed((): AspectRatio | null => {
26+
if (r1.value && r2.value) {
27+
return simplifyRatio(r1.value, r2.value);
28+
}
29+
return null;
30+
});
31+
32+
function updateDimensions(changedField: 'width' | 'height') {
33+
if (aspectRatio.value) {
34+
if (changedField === 'width' && width.value) {
35+
const newDimensions = calculateDimensions(width.value, aspectRatio.value, true);
36+
height.value = newDimensions.height;
37+
}
38+
else if (changedField === 'height' && height.value) {
39+
const newDimensions = calculateDimensions(height.value, aspectRatio.value, false);
40+
width.value = newDimensions.width;
41+
}
42+
}
43+
}
44+
45+
function handlePresetChange(value: string) {
46+
const [newR1, newR2] = value.split(':').map(Number);
47+
r1.value = newR1;
48+
r2.value = newR2;
49+
if (width.value) {
50+
updateDimensions('width');
51+
}
52+
else if (height.value) {
53+
updateDimensions('height');
54+
}
55+
}
56+
57+
watch([r1, r2], () => {
58+
if (r1.value && r2.value) {
59+
if (width.value) {
60+
updateDimensions('width');
61+
}
62+
else if (height.value) {
63+
updateDimensions('height');
64+
}
65+
}
66+
});
67+
</script>
68+
69+
<template>
70+
<NSpace vertical :size="24">
71+
<div>
72+
<h3>Common Presets</h3>
73+
<NSelect
74+
v-model:value="selectedPreset"
75+
:options="presets"
76+
placeholder="Select a preset"
77+
@update:value="handlePresetChange"
78+
/>
79+
</div>
80+
81+
<div class="input-group">
82+
<div class="input-pair">
83+
<label>Pixels width</label>
84+
<NInputNumber v-model:value="width" placeholder="Pixels width" :min="1" @update:value="() => updateDimensions('width')" />
85+
</div>
86+
<div class="input-pair">
87+
<label>Pixels height</label>
88+
<NInputNumber v-model:value="height" placeholder="Pixels height" :min="1" @update:value="() => updateDimensions('height')" />
89+
</div>
90+
</div>
91+
92+
<div class="input-group">
93+
<div class="input-pair">
94+
<label>Ratio width</label>
95+
<NInputNumber v-model:value="r1" placeholder="Ratio width" :min="1" />
96+
</div>
97+
<div class="separator">
98+
:
99+
</div>
100+
<div class="input-pair">
101+
<label>Ratio height</label>
102+
<NInputNumber v-model:value="r2" placeholder="Ratio height" :min="1" />
103+
</div>
104+
</div>
105+
</NSpace>
106+
</template>
107+
108+
<style scoped>
109+
h2 {
110+
font-size: 24px;
111+
margin-bottom: 8px;
112+
}
113+
114+
h3 {
115+
font-size: 18px;
116+
margin-bottom: 8px;
117+
}
118+
119+
p {
120+
margin-bottom: 24px;
121+
color: #a0a0a0;
122+
}
123+
124+
.input-group {
125+
display: flex;
126+
align-items: flex-end;
127+
gap: 16px;
128+
}
129+
130+
.input-pair {
131+
flex: 1;
132+
display: flex;
133+
flex-direction: column;
134+
}
135+
136+
.input-pair label {
137+
margin-bottom: 4px;
138+
color: #a0a0a0;
139+
}
140+
141+
.separator {
142+
align-self: flex-end;
143+
margin-bottom: 7px;
144+
font-size: 24px;
145+
font-weight: bold;
146+
}
147+
148+
.result {
149+
font-size: 18px;
150+
color: #ffffff;
151+
}
152+
153+
:deep(.n-input-number) {
154+
width: 100%;
155+
}
156+
157+
:deep(.n-input-number-input) {
158+
text-align: left;
159+
}
160+
161+
:deep(.n-select) {
162+
width: 100%;
163+
}
164+
165+
:deep(.n-divider) {
166+
margin: 16px 0;
167+
}
168+
</style>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { AspectRatio } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'Aspect Ratio Calculator',
6+
path: '/aspect-ratio-calculator',
7+
description: 'Use this ratio calculator to check the dimensions when resizing images.',
8+
keywords: ['aspect', 'ratio', 'calculator'],
9+
component: () => import('./aspect-ratio-calculator.vue'),
10+
icon: AspectRatio,
11+
createdAt: new Date('2024-08-14'),
12+
});

src/tools/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tool as base64FileConverter } from './base64-file-converter';
22
import { tool as base64StringConverter } from './base64-string-converter';
33
import { tool as basicAuthGenerator } from './basic-auth-generator';
4+
import { tool as aspectRatioCalculator } from './aspect-ratio-calculator';
45

56
import { tool as asciiTextDrawer } from './ascii-text-drawer';
67

@@ -136,7 +137,7 @@ export const toolsByCategory: ToolCategory[] = [
136137
},
137138
{
138139
name: 'Images and videos',
139-
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
140+
components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, aspectRatioCalculator],
140141
},
141142
{
142143
name: 'Development',

0 commit comments

Comments
 (0)