feat: add image filters, update some logic and workflow
This commit is contained in:
@@ -1,12 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { IMAGE_SETTINGS, PANEL_SETTINGS } from "$lib/constants";
|
||||
import { imageState } from "$states/image.svelte";
|
||||
import Konva from "konva";
|
||||
import { onMount } from "svelte";
|
||||
import { Image, Layer, Stage } from "svelte-konva";
|
||||
|
||||
let active = $state(false);
|
||||
let masterImage: Image | undefined = $state();
|
||||
|
||||
let contrast = $derived(imageState.appliedFilters.contrast);
|
||||
let brightness = $derived(imageState.appliedFilters.brightness);
|
||||
let hue = $derived(imageState.appliedFilters.hue);
|
||||
let saturation = $derived(imageState.appliedFilters.saturation);
|
||||
let luminance = $derived(imageState.appliedFilters.luminance);
|
||||
|
||||
function updateMasterImage() {
|
||||
if (masterImage?.node && masterImage?.node?.width() > 0) {
|
||||
masterImage?.node.cache();
|
||||
imageState.masterImage = masterImage?.node.toCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
void contrast;
|
||||
void brightness;
|
||||
void hue;
|
||||
void saturation;
|
||||
void luminance;
|
||||
updateMasterImage();
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
updateMasterImage();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="crop-canvas-container">
|
||||
<img src={imageState.fullImage} alt="Background for use" />
|
||||
<canvas class="crop-canvas"></canvas>
|
||||
<Stage width={PANEL_SETTINGS.PANEL_WIDTH} height={PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT}>
|
||||
<Layer>
|
||||
<Image
|
||||
image={imageState.croppedImage}
|
||||
bind:this={masterImage}
|
||||
filters={[Konva.Filters.Brightness, Konva.Filters.Contrast, Konva.Filters.HSL]}
|
||||
brightness={brightness / IMAGE_SETTINGS.BRIGHTNESS_DIVIDER}
|
||||
{contrast}
|
||||
{hue}
|
||||
saturation={saturation / IMAGE_SETTINGS.SATURATION_DIVIDER}
|
||||
luminance={luminance / IMAGE_SETTINGS.LUMINANCE_DIVIDER}
|
||||
/>
|
||||
</Layer>
|
||||
</Stage>
|
||||
|
||||
<div class="crop-box" class:active>
|
||||
<div class="crop-handle nw"></div>
|
||||
<div class="crop-handle ne"></div>
|
||||
@@ -30,12 +74,6 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.crop-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.crop-box {
|
||||
position: absolute;
|
||||
border: 2px solid var(--action-primary);
|
||||
|
||||
@@ -9,14 +9,10 @@
|
||||
import { imageState } from "$states/image.svelte";
|
||||
import Pencil from "~icons/lucide/pencil";
|
||||
import Reset from "~icons/lucide/rotate-ccw";
|
||||
import Sliders from "~icons/lucide/sliders-horizontal";
|
||||
import Upload from "~icons/lucide/upload";
|
||||
import CropInline from "./CropInline.svelte";
|
||||
|
||||
let brightness = $state(IMAGE_SETTINGS.DEFAULT_BRIGHTNESS);
|
||||
let contrast = $state(IMAGE_SETTINGS.DEFAULT_CONTRAST);
|
||||
let hue = $state(IMAGE_SETTINGS.DEFAULT_HUE);
|
||||
let chroma = $state(IMAGE_SETTINGS.DEFAULT_CHROMA);
|
||||
|
||||
async function handlePaste(event: ClipboardEvent) {
|
||||
let image = await uploadService.fromClipboard(event);
|
||||
if (image.ok) {
|
||||
@@ -35,29 +31,50 @@
|
||||
<Button label="Сбросить" ariaLabel="Reset" icon={Reset} type="outline" />
|
||||
</div>
|
||||
|
||||
<SettingsGrid>
|
||||
<SettingsRow label="Смещение цвета">
|
||||
<RangeSlider bind:value={hue} min={IMAGE_SETTINGS.HUE_MIN} max={IMAGE_SETTINGS.HUE_MAX} />
|
||||
<SettingsGrid label="Настройки изображения" icon={Sliders}>
|
||||
<SettingsRow label="Сдвиг цвета">
|
||||
<RangeSlider
|
||||
bind:value={imageState.hue}
|
||||
min={IMAGE_SETTINGS.HUE_MIN}
|
||||
max={IMAGE_SETTINGS.HUE_MAX}
|
||||
defaultValue={IMAGE_SETTINGS.HUE_DEFAULT}
|
||||
showReset={true}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Насыщенность">
|
||||
<RangeSlider
|
||||
bind:value={chroma}
|
||||
min={IMAGE_SETTINGS.CHROMA_MIN}
|
||||
max={IMAGE_SETTINGS.CHROMA_MAX}
|
||||
bind:value={imageState.saturation}
|
||||
min={IMAGE_SETTINGS.SATURATION_MIN}
|
||||
max={IMAGE_SETTINGS.SATURATION_MAX}
|
||||
defaultValue={IMAGE_SETTINGS.SATURATION_DEFAULT}
|
||||
showReset={true}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Яркость">
|
||||
<RangeSlider
|
||||
bind:value={brightness}
|
||||
bind:value={imageState.luminance}
|
||||
min={IMAGE_SETTINGS.LUMINANCE_MIN}
|
||||
max={IMAGE_SETTINGS.LUMINANCE_MAX}
|
||||
defaultValue={IMAGE_SETTINGS.LUMINANCE_DEFAULT}
|
||||
showReset={true}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Светлота">
|
||||
<RangeSlider
|
||||
bind:value={imageState.brightness}
|
||||
min={IMAGE_SETTINGS.BRIGHTNESS_MIN}
|
||||
max={IMAGE_SETTINGS.BRIGHTNESS_MAX}
|
||||
defaultValue={IMAGE_SETTINGS.BRIGHTNESS_DEFAULT}
|
||||
showReset={true}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow label="Контраст">
|
||||
<RangeSlider
|
||||
bind:value={contrast}
|
||||
bind:value={imageState.contrast}
|
||||
min={IMAGE_SETTINGS.CONTRAST_MIN}
|
||||
max={IMAGE_SETTINGS.CONTRAST_MAX}
|
||||
defaultValue={IMAGE_SETTINGS.CONTRAST_DEFAULT}
|
||||
showReset={true}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</SettingsGrid>
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import Button from "$components/ui/Button.svelte";
|
||||
import type { Component, Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
label: string;
|
||||
icon: Component;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children, label = "fuf", icon }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="settings-grid">
|
||||
{@render children()}
|
||||
<label>
|
||||
{label}
|
||||
<Button {icon} ariaLabel="Expand" type="mini" onclick={() => (expanded = !expanded)} />
|
||||
</label>
|
||||
{#if expanded}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -17,5 +28,20 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--surface-subtle);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-main);
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--text-main);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
let x = $derived(textConfigState.paddingX);
|
||||
let y = $derived(textConfigState.offsetY);
|
||||
let width = $derived(PANEL_SETTINGS.PANEL_WIDTH - 2 * textConfigState.paddingX);
|
||||
let height = PANEL_SETTINGS.PANEL_HEIGHT_DEFAULT;
|
||||
</script>
|
||||
|
||||
<Stage
|
||||
@@ -23,20 +22,17 @@
|
||||
bind:this={stage}
|
||||
>
|
||||
<Layer>
|
||||
<Image image={imageState.croppedImage}></Image>
|
||||
<Image image={imageState.masterImage} listening={false} />
|
||||
<Text
|
||||
{text}
|
||||
{x}
|
||||
{y}
|
||||
{width}
|
||||
{height}
|
||||
fontSize={textConfigState.fontSize}
|
||||
stroke={textConfigState.outlined ? textConfigState.color : "transparent"}
|
||||
fill={textConfigState.outlined ? "transparent" : textConfigState.color}
|
||||
fontFamily={textConfigState.fontFamily}
|
||||
align={textConfigState.align}
|
||||
wrap="none"
|
||||
ellipsis={true}
|
||||
/>
|
||||
</Layer>
|
||||
</Stage>
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
import Button from "$components/ui/Button.svelte";
|
||||
import { PANEL_SETTINGS, TRANSITION_DURATION, type SlideDirectionType } from "$lib/constants";
|
||||
import { downloadService, type DownloadItem } from "$services/downloadService";
|
||||
import { imageState } from "$states/image.svelte";
|
||||
import { konvaAllStagesState } from "$states/konvaAllStages.svelte";
|
||||
import { konvaStageState } from "$states/konvaStage.svelte";
|
||||
import { textsState } from "$states/texts.svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import Download from "~icons/lucide/download";
|
||||
import Loader from "~icons/lucide/loader";
|
||||
import StickyNote from "~icons/lucide/sticky-note";
|
||||
import Preview from "./Preview.svelte";
|
||||
import PreviewAll from "./PreviewAll.svelte";
|
||||
@@ -67,6 +69,9 @@
|
||||
in:fly={{ x: xDirection, duration: TRANSITION_DURATION }}
|
||||
out:fly={{ x: -xDirection, duration: TRANSITION_DURATION }}
|
||||
>
|
||||
<div class="loader" class:loading={imageState.isReady === false}>
|
||||
<Loader />
|
||||
</div>
|
||||
<Preview {text} bind:stage={konvaStageState.stage} />
|
||||
</div>
|
||||
{/key}
|
||||
@@ -141,4 +146,29 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: 0.3s;
|
||||
opacity: 0;
|
||||
&.loading {
|
||||
opacity: 1;
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
}
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
import SelectFont from "$components/ui/SelectFont.svelte";
|
||||
import { TYPOGRAPHY } from "$lib/constants";
|
||||
import { textConfigState } from "$states/textConfig.svelte";
|
||||
import TextSettings from "~icons/lucide/text-initial";
|
||||
</script>
|
||||
|
||||
<Card title="Настройки текста">
|
||||
<SettingsGrid>
|
||||
<SettingsGrid label="Настройки шрифта" icon={TextSettings}>
|
||||
<SettingsRow label="Размер">
|
||||
<RangeSlider
|
||||
bind:value={textConfigState.fontSize}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
class="btn"
|
||||
class:btn-primary={type === "primary"}
|
||||
class:btn-secondary={type === "secondary"}
|
||||
class:btn-outline={type === "outline"}
|
||||
class:btn-outline={type === "outline" || type === "mini"}
|
||||
class:btn-danger={type === "danger"}
|
||||
class:btn-mini={type === "mini"}
|
||||
class:grow={extra === "grow"}
|
||||
@@ -79,6 +79,19 @@
|
||||
--btn-hover: var(--action-secondary-hover);
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-main);
|
||||
border-radius: var(--radius);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-mini:hover:not(:disabled) {
|
||||
border-color: var(--border-main);
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: var(--text-main);
|
||||
@@ -95,19 +108,6 @@
|
||||
--btn-main: var(--danger-base);
|
||||
}
|
||||
|
||||
.btn-mini {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-main);
|
||||
border-radius: var(--radius);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-mini:hover:not(:disabled) {
|
||||
border-color: var(--border-main);
|
||||
}
|
||||
|
||||
.grow {
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type { ChangeEventHandler } from "svelte/elements";
|
||||
import Reset from "~icons/lucide/rotate-ccw";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
defaultValue?: number;
|
||||
showReset?: boolean;
|
||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||
}
|
||||
|
||||
let { value = $bindable(), min = 0, max = 100, step = 1, onchange = () => {} }: Props = $props();
|
||||
let {
|
||||
value = $bindable(),
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
onchange = () => {},
|
||||
defaultValue = 0,
|
||||
showReset = false,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<input type="range" {min} {max} {step} bind:value class="slider" {onchange} />
|
||||
<span class="value-display">{value}</span>
|
||||
{#if showReset}
|
||||
<Button ariaLabel="Reset" icon={Reset} type="mini" onclick={() => (value = defaultValue)} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.slider {
|
||||
|
||||
Reference in New Issue
Block a user