mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: add color picker
This commit is contained in:
@@ -9,42 +9,52 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
params: ParamDef[];
|
params: ParamDef[];
|
||||||
values: Record<string, any>;
|
values: Record<string, any>;
|
||||||
|
pipetteTargetId?: string | null;
|
||||||
|
onPipetteToggle?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { params, values = $bindable() }: Props = $props();
|
let { params, values = $bindable(), pipetteTargetId = null, onPipetteToggle }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each params as param (param.id)}
|
{#each params as param (param.id)}
|
||||||
{#if param.type === 'checkbox'}
|
<div class="field">
|
||||||
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
|
{#if param.type === 'checkbox'}
|
||||||
{:else if param.type === 'number'}
|
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
|
||||||
<TextField
|
{:else if param.type === 'number'}
|
||||||
id={param.id}
|
<TextField
|
||||||
label={param.label}
|
id={param.id}
|
||||||
type="number"
|
label={param.label}
|
||||||
min={param.min}
|
type="number"
|
||||||
max={param.max}
|
min={param.min}
|
||||||
step={param.step}
|
max={param.max}
|
||||||
bind:value={values[param.id]}
|
step={param.step}
|
||||||
/>
|
bind:value={values[param.id]}
|
||||||
{:else if param.type === 'slider'}
|
/>
|
||||||
<SliderField
|
{:else if param.type === 'slider'}
|
||||||
id={param.id}
|
<SliderField
|
||||||
label={param.label}
|
id={param.id}
|
||||||
min={param.min}
|
label={param.label}
|
||||||
max={param.max}
|
min={param.min}
|
||||||
step={param.step}
|
max={param.max}
|
||||||
default={param.default}
|
step={param.step}
|
||||||
bind:value={values[param.id]}
|
default={param.default}
|
||||||
/>
|
bind:value={values[param.id]}
|
||||||
{:else if param.type === 'select'}
|
/>
|
||||||
<SelectField
|
{:else if param.type === 'select'}
|
||||||
id={param.id}
|
<SelectField
|
||||||
label={param.label}
|
id={param.id}
|
||||||
options={param.options}
|
label={param.label}
|
||||||
bind:value={values[param.id]}
|
options={param.options}
|
||||||
/>
|
bind:value={values[param.id]}
|
||||||
{:else if param.type === 'color'}
|
/>
|
||||||
<ColorField id={param.id} label={param.label} bind:value={values[param.id]} />
|
{:else if param.type === 'color'}
|
||||||
{/if}
|
<ColorField
|
||||||
|
id={param.id}
|
||||||
|
label={param.label}
|
||||||
|
bind:value={values[param.id]}
|
||||||
|
pipetteActive={pipetteTargetId === param.id}
|
||||||
|
onPipetteToggle={() => onPipetteToggle?.(param.id)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { rgbToHex } from '$lib/core/color';
|
||||||
import type { PixelImage } from '$lib/core/types';
|
import type { PixelImage } from '$lib/core/types';
|
||||||
|
|
||||||
let { image }: { image: PixelImage | null } = $props();
|
interface Props {
|
||||||
|
image: PixelImage | null;
|
||||||
|
pipetteActive?: boolean;
|
||||||
|
onPickColor?: (hex: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { image, pipetteActive = false, onPickColor }: Props = $props();
|
||||||
|
|
||||||
let canvas = $state<HTMLCanvasElement | undefined>();
|
let canvas = $state<HTMLCanvasElement | undefined>();
|
||||||
|
|
||||||
@@ -13,10 +20,29 @@
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
ctx.putImageData(new ImageData(image.data, image.width, image.height), 0, 0);
|
ctx.putImageData(new ImageData(image.data, image.width, image.height), 0, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function pickColor(event: MouseEvent) {
|
||||||
|
if (!pipetteActive || !onPickColor || !canvas) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if (rect.width === 0 || rect.height === 0) return;
|
||||||
|
const x = Math.floor((event.clientX - rect.left) * (canvas.width / rect.width));
|
||||||
|
const y = Math.floor((event.clientY - rect.top) * (canvas.height / rect.height));
|
||||||
|
if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
const [r, g, b] = ctx.getImageData(x, y, 1, 1).data;
|
||||||
|
onPickColor(rgbToHex(r, g, b));
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if image}
|
{#if image}
|
||||||
<canvas bind:this={canvas} title="{image.width} × {image.height}"></canvas>
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<canvas
|
||||||
|
bind:this={canvas}
|
||||||
|
title="{image.width} × {image.height}"
|
||||||
|
class:pipette={pipetteActive}
|
||||||
|
onclick={pickColor}
|
||||||
|
></canvas>
|
||||||
<p class="dims">{image.width} × {image.height} px</p>
|
<p class="dims">{image.width} × {image.height} px</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -32,9 +58,13 @@
|
|||||||
border-radius: var(--radius-m);
|
border-radius: var(--radius-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas.pipette {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
.dims {
|
.dims {
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.85rem;
|
font-size: var(--text-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,6 +26,17 @@
|
|||||||
let runToken = 0;
|
let runToken = 0;
|
||||||
let lastRunSource: PixelImage | null = null;
|
let lastRunSource: PixelImage | null = null;
|
||||||
let lastRunValuesJson = '';
|
let lastRunValuesJson = '';
|
||||||
|
let pipetteTargetId = $state<string | null>(null);
|
||||||
|
|
||||||
|
function handlePipetteToggle(id: string) {
|
||||||
|
pipetteTargetId = pipetteTargetId === id ? null : id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePickColor(hex: string) {
|
||||||
|
if (!pipetteTargetId) return;
|
||||||
|
values[pipetteTargetId] = hex;
|
||||||
|
pipetteTargetId = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFile(file: File) {
|
async function handleFile(file: File) {
|
||||||
errorText = '';
|
errorText = '';
|
||||||
@@ -36,6 +47,7 @@
|
|||||||
result = null;
|
result = null;
|
||||||
previewResult = null;
|
previewResult = null;
|
||||||
showMask = false;
|
showMask = false;
|
||||||
|
pipetteTargetId = null;
|
||||||
info = isInfo ? imageInfo(source) : null;
|
info = isInfo ? imageInfo(source) : null;
|
||||||
if (isInfo) {
|
if (isInfo) {
|
||||||
status = 'loaded';
|
status = 'loaded';
|
||||||
@@ -90,6 +102,7 @@
|
|||||||
result = null;
|
result = null;
|
||||||
previewResult = null;
|
previewResult = null;
|
||||||
showMask = false;
|
showMask = false;
|
||||||
|
pipetteTargetId = null;
|
||||||
info = null;
|
info = null;
|
||||||
errorText = '';
|
errorText = '';
|
||||||
status = 'idle';
|
status = 'idle';
|
||||||
@@ -132,6 +145,8 @@
|
|||||||
onFile={handleFile}
|
onFile={handleFile}
|
||||||
onError={(message) => (errorText = message)}
|
onError={(message) => (errorText = message)}
|
||||||
onReset={reset}
|
onReset={reset}
|
||||||
|
pipetteActive={!!pipetteTargetId}
|
||||||
|
onPickColor={handlePickColor}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="cell">
|
<div class="cell">
|
||||||
@@ -151,7 +166,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if source && !isInfo}
|
{#if source && !isInfo}
|
||||||
<ParamsCard params={tool.params} bind:values />
|
<ParamsCard
|
||||||
|
params={tool.params}
|
||||||
|
bind:values
|
||||||
|
{pipetteTargetId}
|
||||||
|
onPipetteToggle={handlePipetteToggle}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,17 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
params: ParamDef[];
|
params: ParamDef[];
|
||||||
values: Record<string, any>;
|
values: Record<string, any>;
|
||||||
|
pipetteTargetId?: string | null;
|
||||||
|
onPipetteToggle?: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { params, values = $bindable() }: Props = $props();
|
let { params, values = $bindable(), pipetteTargetId = null, onPipetteToggle }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="panel root">
|
<div class="panel root">
|
||||||
<h2 class="heading-section">Параметры</h2>
|
<h2 class="heading-section">Параметры</h2>
|
||||||
{#if params.length > 0}
|
{#if params.length > 0}
|
||||||
<ParamForm {params} bind:values />
|
<ParamForm {params} bind:values {pipetteTargetId} {onPipetteToggle} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="hint text-caption text-muted">
|
<p class="hint text-caption text-muted">
|
||||||
У этого инструмента нет параметров — результат уже готов.
|
У этого инструмента нет параметров — результат уже готов.
|
||||||
|
|||||||
@@ -10,9 +10,11 @@
|
|||||||
onFile: (file: File) => void;
|
onFile: (file: File) => void;
|
||||||
onError: (message: string) => void;
|
onError: (message: string) => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
pipetteActive?: boolean;
|
||||||
|
onPickColor?: (hex: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { source, onFile, onError, onReset }: Props = $props();
|
let { source, onFile, onError, onReset, pipetteActive = false, onPickColor }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<DropOverlay {onFile} {onError}>
|
<DropOverlay {onFile} {onError}>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<Preview image={source} />
|
<Preview image={source} pipetteActive={pipetteActive} onPickColor={onPickColor} />
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" onclick={onReset}>Заменить изображение</Button>
|
<Button variant="secondary" onclick={onReset}>Заменить изображение</Button>
|
||||||
</DropOverlay>
|
</DropOverlay>
|
||||||
|
|||||||
@@ -6,20 +6,75 @@
|
|||||||
label: string;
|
label: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
|
pipetteActive?: boolean;
|
||||||
|
onPipetteToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { id, label, value = $bindable('#000000'), hint }: Props = $props();
|
let { id, label, value = $bindable('#000000'), hint, pipetteActive = false, onPipetteToggle }:
|
||||||
|
Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field {id} {label} {hint}>
|
<Field {id} {label} {hint}>
|
||||||
<input id={id} class="control swatch" type="color" bind:value />
|
<div class="row">
|
||||||
|
<input id={id} class="control swatch" type="color" bind:value />
|
||||||
|
{#if onPipetteToggle}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pipette"
|
||||||
|
aria-label="Пипетка"
|
||||||
|
aria-pressed={pipetteActive}
|
||||||
|
title="Пипетка"
|
||||||
|
onclick={onPipetteToggle}
|
||||||
|
>
|
||||||
|
◎
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
height: var(--control-height);
|
height: var(--control-height);
|
||||||
width: 3.5rem;
|
width: 3.5rem;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pipette {
|
||||||
|
flex: none;
|
||||||
|
width: var(--control-height);
|
||||||
|
height: var(--control-height);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
color var(--transition-fast),
|
||||||
|
background var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipette:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipette[aria-pressed='true'] {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, var(--surface));
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user