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,12 +9,15 @@
|
||||
interface Props {
|
||||
params: ParamDef[];
|
||||
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>
|
||||
|
||||
{#each params as param (param.id)}
|
||||
<div class="field">
|
||||
{#if param.type === 'checkbox'}
|
||||
<CheckboxField id={param.id} label={param.label} bind:checked={values[param.id]} />
|
||||
{:else if param.type === 'number'}
|
||||
@@ -45,6 +48,13 @@
|
||||
bind:value={values[param.id]}
|
||||
/>
|
||||
{:else if param.type === 'color'}
|
||||
<ColorField id={param.id} label={param.label} bind:value={values[param.id]} />
|
||||
<ColorField
|
||||
id={param.id}
|
||||
label={param.label}
|
||||
bind:value={values[param.id]}
|
||||
pipetteActive={pipetteTargetId === param.id}
|
||||
onPipetteToggle={() => onPipetteToggle?.(param.id)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { rgbToHex } from '$lib/core/color';
|
||||
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>();
|
||||
|
||||
@@ -13,10 +20,29 @@
|
||||
if (!ctx) return;
|
||||
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>
|
||||
|
||||
{#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>
|
||||
{/if}
|
||||
|
||||
@@ -32,9 +58,13 @@
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
|
||||
canvas.pipette {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.dims {
|
||||
margin-top: var(--space-1);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-size: var(--text-s);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +26,17 @@
|
||||
let runToken = 0;
|
||||
let lastRunSource: PixelImage | null = null;
|
||||
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) {
|
||||
errorText = '';
|
||||
@@ -36,6 +47,7 @@
|
||||
result = null;
|
||||
previewResult = null;
|
||||
showMask = false;
|
||||
pipetteTargetId = null;
|
||||
info = isInfo ? imageInfo(source) : null;
|
||||
if (isInfo) {
|
||||
status = 'loaded';
|
||||
@@ -90,6 +102,7 @@
|
||||
result = null;
|
||||
previewResult = null;
|
||||
showMask = false;
|
||||
pipetteTargetId = null;
|
||||
info = null;
|
||||
errorText = '';
|
||||
status = 'idle';
|
||||
@@ -132,6 +145,8 @@
|
||||
onFile={handleFile}
|
||||
onError={(message) => (errorText = message)}
|
||||
onReset={reset}
|
||||
pipetteActive={!!pipetteTargetId}
|
||||
onPickColor={handlePickColor}
|
||||
/>
|
||||
</div>
|
||||
<div class="cell">
|
||||
@@ -151,7 +166,12 @@
|
||||
</div>
|
||||
|
||||
{#if source && !isInfo}
|
||||
<ParamsCard params={tool.params} bind:values />
|
||||
<ParamsCard
|
||||
params={tool.params}
|
||||
bind:values
|
||||
{pipetteTargetId}
|
||||
onPipetteToggle={handlePipetteToggle}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
interface Props {
|
||||
params: ParamDef[];
|
||||
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>
|
||||
|
||||
<div class="panel root">
|
||||
<h2 class="heading-section">Параметры</h2>
|
||||
{#if params.length > 0}
|
||||
<ParamForm {params} bind:values />
|
||||
<ParamForm {params} bind:values {pipetteTargetId} {onPipetteToggle} />
|
||||
{:else}
|
||||
<p class="hint text-caption text-muted">
|
||||
У этого инструмента нет параметров — результат уже готов.
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
onFile: (file: File) => void;
|
||||
onError: (message: string) => 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>
|
||||
|
||||
<div class="container">
|
||||
@@ -22,7 +24,7 @@
|
||||
{:else}
|
||||
<DropOverlay {onFile} {onError}>
|
||||
<div class="media">
|
||||
<Preview image={source} />
|
||||
<Preview image={source} pipetteActive={pipetteActive} onPickColor={onPickColor} />
|
||||
</div>
|
||||
<Button variant="secondary" onclick={onReset}>Заменить изображение</Button>
|
||||
</DropOverlay>
|
||||
|
||||
@@ -6,20 +6,75 @@
|
||||
label: string;
|
||||
value?: 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>
|
||||
|
||||
<Field {id} {label} {hint}>
|
||||
<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>
|
||||
|
||||
<style>
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
height: var(--control-height);
|
||||
width: 3.5rem;
|
||||
padding: 2px;
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user