openpencil/src/components/GradientEditor.vue
2026-06-30 10:54:32 +03:00

129 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import AppSelect from './ui/AppSelect.vue'
import Tip from './ui/Tip.vue'
import ColorPickerPanel from '@/components/ColorPickerPanel/ColorPickerPanel.vue'
import ScrubInput from './ScrubInput.vue'
import { colorToCSS } from '@open-pencil/core/color'
import {
GradientEditorRoot,
GradientEditorBar,
GradientEditorStop,
inputValue,
useI18n
} from '@open-pencil/vue'
import type { Fill } from '@open-pencil/scene-graph'
const { fill } = defineProps<{ fill: Fill }>()
const emit = defineEmits<{ update: [fill: Fill] }>()
const { panels } = useI18n()
</script>
<template>
<GradientEditorRoot :fill="fill" @update="emit('update', $event)" v-slot="root">
<div>
<div class="mb-2 w-28">
<AppSelect
:model-value="root.subtype"
:options="root.subtypes"
@update:model-value="root.actions.setSubtype($event)"
/>
</div>
<GradientEditorBar
:stops="root.stops"
:active-stop-index="root.activeStopIndex"
:bar-background="root.barBackground"
:ui="{ bar: 'relative mb-2 h-6 rounded' }"
data-test-id="fill-picker-gradient-bar"
@select-stop="root.actions.selectStop"
@drag-stop="root.actions.dragStop"
v-slot="bar"
>
<div
v-for="(stop, idx) in bar.stops"
:key="idx"
class="absolute top-1/2 size-3.5 -translate-x-1/2 -translate-y-1/2 cursor-grab rounded-sm border-2 shadow-sm"
:class="idx === bar.activeStopIndex ? 'border-white' : 'border-white/60'"
:style="{ left: `${stop.position * 100}%`, background: colorToCSS(stop.color) }"
@pointerdown.stop="bar.actions.stopPointerDown(idx, $event)"
/>
</GradientEditorBar>
<div class="mb-2">
<div class="mb-1 flex items-center justify-between">
<span class="text-[11px] text-muted">{{ panels.stops }}</span>
<Tip :label="panels.addStop">
<button
class="flex size-4 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:text-surface"
data-test-id="fill-picker-add-stop"
@click="root.actions.addStop"
>
<icon-lucide-plus class="size-3" />
</button>
</Tip>
</div>
<GradientEditorStop
v-for="(stop, idx) in root.stops"
:key="idx"
:stop="stop"
:index="idx"
:active="idx === root.activeStopIndex"
@select="root.actions.selectStop"
@update-position="root.actions.updateStopPosition"
@update-color="root.actions.updateStopColor"
@update-opacity="root.actions.updateStopOpacity"
@remove="root.actions.removeStop"
v-slot="s"
>
<div
class="flex items-center gap-1 py-0.5"
:class="{ 'rounded bg-hover/50': s.active }"
@click="s.actions.select"
>
<ScrubInput
class="w-11"
suffix="%"
:model-value="s.positionPercent"
:min="0"
:max="100"
@update:model-value="s.actions.updatePosition(Number($event))"
@click.stop
/>
<button
class="size-4 shrink-0 cursor-pointer rounded border border-border p-0"
:style="{ background: s.css }"
@click.stop="s.actions.select"
/>
<input
class="min-w-0 flex-1 rounded border border-border bg-input px-1 py-0.5 font-mono text-[11px] text-surface"
:value="s.hex"
maxlength="6"
@change="s.actions.updateColor(inputValue($event))"
@click.stop
/>
<ScrubInput
class="w-9"
suffix="%"
:model-value="s.opacityPercent"
:min="0"
:max="100"
@update:model-value="s.actions.updateOpacity(Number($event))"
@click.stop
/>
<button
v-if="root.stops.length > 2"
class="flex size-4 cursor-pointer items-center justify-center rounded border-none bg-transparent p-0 text-muted hover:text-surface"
@click.stop="s.actions.remove"
>
−
</button>
</div>
</GradientEditorStop>
</div>
<ColorPickerPanel :color="root.activeColor" @update="root.actions.updateActiveColor" />
</div>
</GradientEditorRoot>
</template>