refactor: refresh from template
This commit is contained in:
+342
-351
@@ -1,26 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, useTemplateRef } from "vue";
|
||||
import LucideIcon from "./components/LucideIcon.vue";
|
||||
import { createIcons, icons } from "lucide";
|
||||
import { ref, onMounted, useTemplateRef, nextTick, onUpdated } from "vue";
|
||||
|
||||
enum Filter {
|
||||
None = "None",
|
||||
Gray = "Gray",
|
||||
Sepia = "Sepia",
|
||||
Invert = "Invert",
|
||||
Blur = "Blur",
|
||||
Brightness = "Brightness",
|
||||
Contrast = "Contrast",
|
||||
HueShift = "Hue shift",
|
||||
Saturate = "Saturate",
|
||||
None = "None",
|
||||
Gray = "Gray",
|
||||
Sepia = "Sepia",
|
||||
Invert = "Invert",
|
||||
Blur = "Blur",
|
||||
Brightness = "Brightness",
|
||||
Contrast = "Contrast",
|
||||
HueShift = "Hue shift",
|
||||
Saturate = "Saturate",
|
||||
}
|
||||
enum PAPosition {
|
||||
None = "None",
|
||||
BL = "Bottom Left",
|
||||
BC = "Bottom Center",
|
||||
BR = "Bottom Right",
|
||||
TL = "Top Left",
|
||||
TC = "Top Center",
|
||||
TR = "Top Right",
|
||||
None = "None",
|
||||
BL = "Bottom Left",
|
||||
BC = "Bottom Center",
|
||||
BR = "Bottom Right",
|
||||
TL = "Top Left",
|
||||
TC = "Top Center",
|
||||
TR = "Top Right",
|
||||
}
|
||||
|
||||
const PA_RATIO = 404 / 646;
|
||||
@@ -46,397 +46,388 @@ const parentalAdvisory = useTemplateRef<HTMLImageElement>("parentalAdvisory");
|
||||
const canvas = useTemplateRef<HTMLCanvasElement>("canvas");
|
||||
|
||||
function openImage() {
|
||||
if (!input.value?.files?.length) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
srcData.value = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(input.value.files[0] as Blob);
|
||||
if (!input.value?.files?.length) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
srcData.value = reader.result as string;
|
||||
};
|
||||
reader.readAsDataURL(input.value.files[0] as Blob);
|
||||
}
|
||||
|
||||
function imageOnLoad() {
|
||||
srcLoaded.value = true;
|
||||
setTimeout(draw);
|
||||
srcLoaded.value = true;
|
||||
setTimeout(draw);
|
||||
}
|
||||
|
||||
function randomElement<T>(items: T[]): T {
|
||||
// @ts-expect-error: arbitrary type bullshit
|
||||
return items[Math.floor(Math.random() * items.length)];
|
||||
// @ts-expect-error: arbitrary type bullshit
|
||||
return items[Math.floor(Math.random() * items.length)];
|
||||
}
|
||||
|
||||
function randomize() {
|
||||
centerX.value = 0.25 + Math.random() * 0.5;
|
||||
centerY.value = 0.25 + Math.random() * 0.5;
|
||||
zoom.value = 1 + Math.random();
|
||||
filter.value = randomElement(Object.values(Filter));
|
||||
filterValue.value = 0.5 + Math.random() * 2;
|
||||
paPos.value = randomElement(Object.values(PAPosition));
|
||||
paScale.value = 0.1 + Math.random() * 0.2;
|
||||
paMargin.value = Math.random() * 0.1;
|
||||
asyncDraw();
|
||||
centerX.value = 0.25 + Math.random() * 0.5;
|
||||
centerY.value = 0.25 + Math.random() * 0.5;
|
||||
zoom.value = 1 + Math.random();
|
||||
filter.value = randomElement(Object.values(Filter));
|
||||
filterValue.value = 0.5 + Math.random() * 2;
|
||||
paPos.value = randomElement(Object.values(PAPosition));
|
||||
paScale.value = 0.1 + Math.random() * 0.2;
|
||||
paMargin.value = Math.random() * 0.1;
|
||||
asyncDraw();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
centerX.value = 0.5;
|
||||
centerY.value = 0.5;
|
||||
zoom.value = 1;
|
||||
filter.value = Filter.None;
|
||||
filterValue.value = 1;
|
||||
paPos.value = PAPosition.BR;
|
||||
paScale.value = 0.2;
|
||||
paMargin.value = 0.05;
|
||||
asyncDraw();
|
||||
centerX.value = 0.5;
|
||||
centerY.value = 0.5;
|
||||
zoom.value = 1;
|
||||
filter.value = Filter.None;
|
||||
filterValue.value = 1;
|
||||
paPos.value = PAPosition.BR;
|
||||
paScale.value = 0.2;
|
||||
paMargin.value = 0.05;
|
||||
asyncDraw();
|
||||
}
|
||||
|
||||
function newImage() {
|
||||
srcLoaded.value = false;
|
||||
reset();
|
||||
srcLoaded.value = false;
|
||||
reset();
|
||||
}
|
||||
|
||||
function download() {
|
||||
const link = document.createElement("a");
|
||||
link.download = `coverify-${new Date().getTime().toString()}.png`;
|
||||
if (targetSize.value !== PREVIEW_SIZE) {
|
||||
draw(targetSize.value);
|
||||
}
|
||||
link.href = canvas.value?.toDataURL() ?? "#";
|
||||
link.click();
|
||||
const link = document.createElement("a");
|
||||
link.download = `coverify-${new Date().getTime().toString()}.png`;
|
||||
if (targetSize.value !== PREVIEW_SIZE) {
|
||||
draw(targetSize.value);
|
||||
}
|
||||
link.href = canvas.value?.toDataURL() ?? "#";
|
||||
link.click();
|
||||
}
|
||||
|
||||
const drawTimeout = ref<number | undefined>(undefined);
|
||||
|
||||
function asyncDraw() {
|
||||
clearTimeout(drawTimeout.value);
|
||||
drawTimeout.value = setTimeout(draw);
|
||||
clearTimeout(drawTimeout.value);
|
||||
drawTimeout.value = setTimeout(draw);
|
||||
}
|
||||
|
||||
function draw(size = PREVIEW_SIZE) {
|
||||
if (!canvas.value) {
|
||||
return;
|
||||
if (!canvas.value) {
|
||||
return;
|
||||
}
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (!ctx || !image.value) {
|
||||
return;
|
||||
}
|
||||
canvas.value.width = size;
|
||||
canvas.value.height = size;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
|
||||
const imgRatio = image.value.height / image.value.width;
|
||||
const widthFirst = image.value.width < image.value.height;
|
||||
const imgWidth = size * (widthFirst ? 1 : 1 / imgRatio) * zoom.value;
|
||||
const imgHeight = size * (widthFirst ? imgRatio : 1) * zoom.value;
|
||||
const dx = (size - imgWidth) * centerX.value;
|
||||
const dy = (size - imgHeight) * centerY.value;
|
||||
|
||||
switch (filter.value) {
|
||||
case Filter.Gray:
|
||||
ctx.filter = `grayscale(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Sepia:
|
||||
ctx.filter = `sepia(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Invert:
|
||||
ctx.filter = `invert(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Blur:
|
||||
ctx.filter = `blur(${((filterValue.value * 2 * size) / PREVIEW_SIZE).toFixed(2)}px)`;
|
||||
break;
|
||||
case Filter.Brightness:
|
||||
ctx.filter = `brightness(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Contrast:
|
||||
ctx.filter = `contrast(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.HueShift:
|
||||
ctx.filter = `hue-rotate(${(filterValue.value * 360).toFixed(2)}deg)`;
|
||||
break;
|
||||
case Filter.Saturate:
|
||||
ctx.filter = `saturate(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.drawImage(image.value, dx, dy, imgWidth, imgHeight);
|
||||
|
||||
ctx.filter = "none";
|
||||
|
||||
if (paPos.value !== PAPosition.None && parentalAdvisory.value) {
|
||||
const paWidth = size * paScale.value;
|
||||
const paHeight = paWidth * PA_RATIO;
|
||||
let padx = paMargin.value * size;
|
||||
let pady = paMargin.value * size;
|
||||
if (
|
||||
paPos.value === PAPosition.BC ||
|
||||
paPos.value === PAPosition.BL ||
|
||||
paPos.value === PAPosition.BR
|
||||
) {
|
||||
pady = size - paHeight - pady;
|
||||
}
|
||||
const ctx = canvas.value.getContext("2d");
|
||||
if (!ctx || !image.value) {
|
||||
return;
|
||||
if (paPos.value === PAPosition.BR || paPos.value === PAPosition.TR) {
|
||||
padx = size - paWidth - padx;
|
||||
} else if (paPos.value === PAPosition.BC || paPos.value === PAPosition.TC) {
|
||||
padx = (size - paWidth) * 0.5;
|
||||
}
|
||||
canvas.value.width = size;
|
||||
canvas.value.height = size;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.drawImage(parentalAdvisory.value, padx, pady, paWidth, paHeight);
|
||||
}
|
||||
}
|
||||
|
||||
const imgRatio = image.value.height / image.value.width;
|
||||
const widthFirst = image.value.width < image.value.height;
|
||||
const imgWidth = size * (widthFirst ? 1 : 1 / imgRatio) * zoom.value;
|
||||
const imgHeight = size * (widthFirst ? imgRatio : 1) * zoom.value;
|
||||
const dx = (size - imgWidth) * centerX.value;
|
||||
const dy = (size - imgHeight) * centerY.value;
|
||||
|
||||
switch (filter.value) {
|
||||
case Filter.Gray:
|
||||
ctx.filter = `grayscale(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Sepia:
|
||||
ctx.filter = `sepia(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Invert:
|
||||
ctx.filter = `invert(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Blur:
|
||||
ctx.filter = `blur(${((filterValue.value * 2 * size) / PREVIEW_SIZE).toFixed(2)}px)`;
|
||||
break;
|
||||
case Filter.Brightness:
|
||||
ctx.filter = `brightness(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.Contrast:
|
||||
ctx.filter = `contrast(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
case Filter.HueShift:
|
||||
ctx.filter = `hue-rotate(${(filterValue.value * 360).toFixed(2)}deg)`;
|
||||
break;
|
||||
case Filter.Saturate:
|
||||
ctx.filter = `saturate(${(filterValue.value * 100).toFixed(2)}%)`;
|
||||
break;
|
||||
}
|
||||
|
||||
ctx.drawImage(image.value, dx, dy, imgWidth, imgHeight);
|
||||
|
||||
ctx.filter = "none";
|
||||
|
||||
if (paPos.value !== PAPosition.None && parentalAdvisory.value) {
|
||||
const paWidth = size * paScale.value;
|
||||
const paHeight = paWidth * PA_RATIO;
|
||||
let padx = paMargin.value * size;
|
||||
let pady = paMargin.value * size;
|
||||
if (
|
||||
paPos.value === PAPosition.BC ||
|
||||
paPos.value === PAPosition.BL ||
|
||||
paPos.value === PAPosition.BR
|
||||
) {
|
||||
pady = size - paHeight - pady;
|
||||
}
|
||||
if (paPos.value === PAPosition.BR || paPos.value === PAPosition.TR) {
|
||||
padx = size - paWidth - padx;
|
||||
} else if (
|
||||
paPos.value === PAPosition.BC ||
|
||||
paPos.value === PAPosition.TC
|
||||
) {
|
||||
padx = (size - paWidth) * 0.5;
|
||||
}
|
||||
ctx.drawImage(parentalAdvisory.value, padx, pady, paWidth, paHeight);
|
||||
}
|
||||
async function updateIcons() {
|
||||
await nextTick();
|
||||
createIcons({
|
||||
icons,
|
||||
nameAttr: "icon",
|
||||
attrs: {
|
||||
width: "1.1em",
|
||||
height: "1.1em",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
setTimeout(() => {
|
||||
visible.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
onUpdated(updateIcons);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main :style="{ display: visible ? 'inherit' : 'none' }">
|
||||
<h1>
|
||||
<LucideIcon name="disc-3" />
|
||||
Coverify
|
||||
</h1>
|
||||
<p><i>Create album covers from mundane photos</i></p>
|
||||
<img
|
||||
ref="image"
|
||||
style="display: none"
|
||||
:src="srcData ?? undefined"
|
||||
@load="imageOnLoad"
|
||||
/>
|
||||
<img
|
||||
ref="parentalAdvisory"
|
||||
style="display: none"
|
||||
src="./parental_advisory.png"
|
||||
/>
|
||||
<input
|
||||
v-if="!srcLoaded"
|
||||
ref="input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="openImage"
|
||||
/>
|
||||
<canvas
|
||||
v-show="srcLoaded"
|
||||
ref="canvas"
|
||||
:width="targetSize"
|
||||
:height="targetSize"
|
||||
></canvas>
|
||||
<br />
|
||||
<template v-if="srcLoaded">
|
||||
<div class="buttons">
|
||||
<button @click="randomize">
|
||||
<LucideIcon name="dices" /> Randomize
|
||||
</button>
|
||||
<button @click="reset">
|
||||
<LucideIcon name="rotate-ccw" /> Reset
|
||||
</button>
|
||||
<button @click="newImage">
|
||||
<LucideIcon name="trash-2" /> New picture
|
||||
</button>
|
||||
<button @click="download">
|
||||
<LucideIcon name="arrow-down-to-line" /> Download
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<table class="config">
|
||||
<colgroup>
|
||||
<col style="width: 25%" />
|
||||
<col />
|
||||
<col style="width: 10%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="center-x">Center X:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="center-x"
|
||||
v-model="centerX"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (centerX * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="center-y">Center Y:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="center-y"
|
||||
v-model="centerY"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (centerY * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="zoom">Zoom:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="zoom"
|
||||
v-model="zoom"
|
||||
type="range"
|
||||
min="1"
|
||||
max="4"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (zoom * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="filter">Filter:</label></td>
|
||||
<td>
|
||||
<select
|
||||
id="filter"
|
||||
v-model="filter"
|
||||
@change="asyncDraw"
|
||||
>
|
||||
<option
|
||||
v-for="value in Object.values(Filter)"
|
||||
:key="`filter-${value}`"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="filter-value">Filter Value:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="filter-value"
|
||||
v-model="filterValue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (filterValue * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-pos">Sticker Position:</label></td>
|
||||
<td>
|
||||
<select
|
||||
id="pa-pos"
|
||||
v-model="paPos"
|
||||
@change="asyncDraw"
|
||||
>
|
||||
<option
|
||||
v-for="value in Object.values(PAPosition)"
|
||||
:key="`pa-pos-${value}`"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-scale">Sticker Scale:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="pa-scale"
|
||||
v-model="paScale"
|
||||
type="range"
|
||||
min="0.10"
|
||||
max="0.30"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (paScale * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-margin">Sticker Margin:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="pa-margin"
|
||||
v-model="paMargin"
|
||||
type="range"
|
||||
min="0"
|
||||
max="0.1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (paMargin * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="size">Image Size:</label></td>
|
||||
<td>
|
||||
<select
|
||||
id="size"
|
||||
v-model="targetSize"
|
||||
@change="asyncDraw"
|
||||
>
|
||||
<option :value="512">512x512</option>
|
||||
<option :value="1024">1024x1024</option>
|
||||
<option :value="2048">2048x2048</option>
|
||||
<option :value="4096">4096x4096</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<br />
|
||||
<hr />
|
||||
<small class="footer">
|
||||
<LucideIcon name="at-sign" />
|
||||
<a href="https://github.com/klemek" target="_blank">klemek</a>
|
||||
-
|
||||
<LucideIcon name="github" />
|
||||
<a href="https://github.com/klemek/coverify" target="_blank"
|
||||
>Repository</a
|
||||
>
|
||||
- 2025
|
||||
</small>
|
||||
</main>
|
||||
<main :style="{ display: visible ? 'inherit' : 'none' }">
|
||||
<h1>
|
||||
<i icon="disc-3"></i>
|
||||
Coverify
|
||||
</h1>
|
||||
<p><i>Create album covers from mundane photos</i></p>
|
||||
<img
|
||||
ref="image"
|
||||
style="display: none"
|
||||
:src="srcData ?? undefined"
|
||||
@load="imageOnLoad"
|
||||
/>
|
||||
<img
|
||||
ref="parentalAdvisory"
|
||||
style="display: none"
|
||||
src="./parental_advisory.png"
|
||||
/>
|
||||
<input
|
||||
v-if="!srcLoaded"
|
||||
ref="input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@change="openImage"
|
||||
/>
|
||||
<canvas
|
||||
v-show="srcLoaded"
|
||||
ref="canvas"
|
||||
:width="targetSize"
|
||||
:height="targetSize"
|
||||
></canvas>
|
||||
<br />
|
||||
<template v-if="srcLoaded">
|
||||
<div class="buttons">
|
||||
<button @click="randomize"><i icon="dices"></i> Randomize</button>
|
||||
<button @click="reset"><i icon="rotate-ccw"></i> Reset</button>
|
||||
<button @click="newImage"><i icon="trash-2"></i> New picture</button>
|
||||
<button @click="download">
|
||||
<i icon="arrow-down-to-line"></i> Download
|
||||
</button>
|
||||
</div>
|
||||
<br />
|
||||
<table class="config">
|
||||
<colgroup>
|
||||
<col style="width: 25%" />
|
||||
<col />
|
||||
<col style="width: 10%" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label for="center-x">Center X:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="center-x"
|
||||
v-model="centerX"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (centerX * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="center-y">Center Y:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="center-y"
|
||||
v-model="centerY"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (centerY * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="zoom">Zoom:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="zoom"
|
||||
v-model="zoom"
|
||||
type="range"
|
||||
min="1"
|
||||
max="4"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (zoom * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="filter">Filter:</label></td>
|
||||
<td>
|
||||
<select id="filter" v-model="filter" @change="asyncDraw">
|
||||
<option
|
||||
v-for="value in Object.values(Filter)"
|
||||
:key="`filter-${value}`"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="filter-value">Filter Value:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="filter-value"
|
||||
v-model="filterValue"
|
||||
type="range"
|
||||
min="0"
|
||||
max="3"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (filterValue * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-pos">Sticker Position:</label></td>
|
||||
<td>
|
||||
<select id="pa-pos" v-model="paPos" @change="asyncDraw">
|
||||
<option
|
||||
v-for="value in Object.values(PAPosition)"
|
||||
:key="`pa-pos-${value}`"
|
||||
>
|
||||
{{ value }}
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-scale">Sticker Scale:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="pa-scale"
|
||||
v-model="paScale"
|
||||
type="range"
|
||||
min="0.10"
|
||||
max="0.30"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (paScale * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="pa-margin">Sticker Margin:</label></td>
|
||||
<td>
|
||||
<input
|
||||
id="pa-margin"
|
||||
v-model="paMargin"
|
||||
type="range"
|
||||
min="0"
|
||||
max="0.1"
|
||||
step="0.01"
|
||||
@input="asyncDraw"
|
||||
/>
|
||||
</td>
|
||||
<td>{{ (paMargin * 100).toFixed(0) }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="size">Image Size:</label></td>
|
||||
<td>
|
||||
<select id="size" v-model="targetSize" @change="asyncDraw">
|
||||
<option :value="512">512x512</option>
|
||||
<option :value="1024">1024x1024</option>
|
||||
<option :value="2048">2048x2048</option>
|
||||
<option :value="4096">4096x4096</option>
|
||||
</select>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
<br />
|
||||
<hr />
|
||||
<small class="footer">
|
||||
<i icon="at-sign"></i>
|
||||
<a href="https://git.klemek.fr/klemek" target="_blank">Kleπek</a>
|
||||
| <i icon="git-branch"></i>
|
||||
<a href="https://git.klemek.fr/klemek/coverify" target="_blank"
|
||||
>Repository</a
|
||||
> | <i icon="copyright"></i> 2025
|
||||
</small>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.buttons > button {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: min(40vh, 100%);
|
||||
margin: auto;
|
||||
width: min(40vh, 100%);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
select,
|
||||
input {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--background-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
}
|
||||
|
||||
.footer {
|
||||
opacity: 50%;
|
||||
opacity: 50%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import * as icons from "lucide-vue-next";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
color?: string;
|
||||
strokeWidth?: string;
|
||||
defaultClass?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: "currentColor",
|
||||
strokeWidth: "2",
|
||||
defaultClass: "lucide",
|
||||
});
|
||||
|
||||
function kebab2camel(kebab: string): string {
|
||||
return kebab
|
||||
.split("-")
|
||||
.map(
|
||||
(item) =>
|
||||
item.charAt(0).toUpperCase() + item.slice(1).toLowerCase(),
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// @ts-expect-error: cannot infer type of all exported data
|
||||
const icon = computed(() => icons[kebab2camel(props.name)]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="icon"
|
||||
:color="color"
|
||||
:stroke-width="strokeWidth"
|
||||
:default-class="defaultClass"
|
||||
/>
|
||||
</template>
|
||||
File diff suppressed because it is too large
Load Diff
+204
@@ -0,0 +1,204 @@
|
||||
@use "./material-colors.css";
|
||||
|
||||
/*
|
||||
=================================================
|
||||
https://www.joshwcomeau.com/css/custom-css-reset/
|
||||
=================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Use a more-intuitive box-sizing model.
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/*
|
||||
2. Remove default margin
|
||||
*/
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
/*
|
||||
3. Allow percentage-based heights in the application
|
||||
*/
|
||||
html,
|
||||
body,
|
||||
div#app {
|
||||
height: 100%;
|
||||
}
|
||||
/*
|
||||
Typographic tweaks!
|
||||
4. Add accessible line-height
|
||||
5. Improve text rendering
|
||||
*/
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
/*
|
||||
6. Improve media defaults
|
||||
*/
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
/*
|
||||
7. Remove built-in form typography styles
|
||||
*/
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
/*
|
||||
8. Avoid text overflows
|
||||
*/
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
/*
|
||||
9. Create a root stacking context
|
||||
*/
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/*
|
||||
=================================================
|
||||
CUSTOM STYLE
|
||||
=================================================
|
||||
*/
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap");
|
||||
@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap");
|
||||
|
||||
:root {
|
||||
/* https://materialui.co/colors/ */
|
||||
--hue-primary: 199.53;
|
||||
--sat-primary: 28.3%;
|
||||
--background: hsl(var(--hue-primary), var(--sat-primary), 96.08%);
|
||||
--background-primary: hsl(var(--hue-primary), var(--sat-primary), 93.33%);
|
||||
--background-secondary: hsl(var(--hue-primary), var(--sat-primary), 90%);
|
||||
--color-primary: hsl(var(--hue-primary), var(--sat-primary), 50%);
|
||||
--text-primary: hsl(var(--hue-primary), var(--sat-primary), 25%);
|
||||
--text-secondary: hsl(var(--hue-primary), var(--sat-primary), 30%);
|
||||
}
|
||||
|
||||
/*
|
||||
=================================================
|
||||
https://blog.koley.in/2019/339-bytes-of-responsive-css
|
||||
https://www.swyx.io/css-100-bytes
|
||||
https://gist.github.com/JoeyBurzynski/617fb6201335779f8424ad9528b72c41
|
||||
=================================================
|
||||
*/
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
color: var(--text-primary);
|
||||
font-family: "Roboto", Verdana, serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1.5rem;
|
||||
margin: auto;
|
||||
background-color: var(--background-primary);
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 1em 0 0.5em;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 2em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
hr {
|
||||
opacity: 25%;
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
textarea,
|
||||
input,
|
||||
select,
|
||||
.mono {
|
||||
font-family: "Roboto Mono", monospace;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
main {
|
||||
max-width: 42rem;
|
||||
}
|
||||
table {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* LUCIDE ICONS
|
||||
*/
|
||||
|
||||
.lucide {
|
||||
width: 1.1em;
|
||||
height: 1.1em;
|
||||
}
|
||||
|
||||
svg.lucide {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
b .lucide,
|
||||
h1 .lucide,
|
||||
h2 .lucide,
|
||||
h3 .lucide,
|
||||
h4 .lucide,
|
||||
h5 .lucide,
|
||||
h6 .lucide {
|
||||
stroke-width: 3;
|
||||
}
|
||||
Reference in New Issue
Block a user