0.4.0: clipart drawer, animation paths, bezier keyframes
- Clipart library: new left-edge drawer (5px idle, expands on hover, 400px when open). User picks a local folder via the File System Access API in settings; directory handle is persisted in IndexedDB so the library survives reloads. Dragging a thumbnail onto the stage creates an image asset + layer at the drop point. - Animation paths on the stage: selecting a keyframe draws the tween path from the previous keyframe to the selected one as a solid polyline with per-frame draggable dots. All other segments on the same position-track render as dashed ghost lines so the full curve progression is visible. - Path dots grow on hover (2.5 → 6 px), show a grab cursor, and have a wider hit area so they're easier to grab. - Bezier-curved position interpolation: keyframes gain optional ctrlX/ctrlY fields. When set, x/y tween along a quadratic bezier through the control point instead of a straight line. Dragging a path dot solves for the control so the curve passes through the drag point at its frame progress. Mirrored across all four render pipelines. - Tests: new suites for polygon actions, freeform drawing lifecycle, polygon + stroke rendering (15 new tests). Fixed the mask-render tests that relied on a default-transparent canvas background. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
276d621d20
commit
869b67458a
16 files changed
+1379
-15
No files matched your search
@@ -1,7 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { PhClock, PhFrameCorners, PhMoon, PhPalette, PhSun } from '@phosphor-icons/vue'
|
||||
import {
|
||||
PhClock,
|
||||
PhFolder,
|
||||
PhFrameCorners,
|
||||
PhMoon,
|
||||
PhPalette,
|
||||
PhSun,
|
||||
} from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { useClipartStore } from '@/stores/clipart'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import type { Theme } from '@/composables/useTheme'
|
||||
import { Modal } from '@/components/ui'
|
||||
@@ -11,13 +19,15 @@ defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
|
||||
const store = useSceneStore()
|
||||
const clipart = useClipartStore()
|
||||
const { theme, setTheme } = useTheme()
|
||||
|
||||
type TabId = 'document' | 'timeline' | 'theme'
|
||||
type TabId = 'document' | 'timeline' | 'clipart' | 'theme'
|
||||
|
||||
const tabs: { id: TabId; label: string; icon: typeof PhFrameCorners }[] = [
|
||||
{ id: 'document', label: t('settings.tab.document'), icon: PhFrameCorners },
|
||||
{ id: 'timeline', label: t('settings.tab.timeline'), icon: PhClock },
|
||||
{ id: 'clipart', label: t('settings.tab.clipart'), icon: PhFolder },
|
||||
{ id: 'theme', label: t('settings.tab.theme'), icon: PhPalette },
|
||||
]
|
||||
|
||||
@@ -145,6 +155,41 @@ function applyTheme(t: Theme) {
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'clipart'">
|
||||
<h3>{{ t('settings.tab.clipart') }}</h3>
|
||||
<div class="clipart-row">
|
||||
<div class="clipart-label">{{ t('clipart.folderLabel') }}</div>
|
||||
<div class="clipart-value">
|
||||
<PhFolder :size="13" />
|
||||
<span v-if="clipart.folderName" class="clipart-folder-name">
|
||||
{{ clipart.folderName }}
|
||||
</span>
|
||||
<span v-else class="clipart-folder-empty">{{ t('clipart.noFolder') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clipart-actions">
|
||||
<button
|
||||
class="primary-btn"
|
||||
:disabled="!clipart.supported"
|
||||
@click="clipart.pickFolder()"
|
||||
>
|
||||
{{ t('clipart.chooseFolder') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="clipart.handle"
|
||||
class="ghost-btn"
|
||||
@click="clipart.clearFolder()"
|
||||
>
|
||||
{{ t('clipart.clearFolder') }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!clipart.supported" class="error-line">
|
||||
{{ t('clipart.unsupported') }}
|
||||
</p>
|
||||
<p v-if="clipart.error" class="error-line">{{ clipart.error }}</p>
|
||||
<p class="hint">{{ t('clipart.settingsHint') }}</p>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeTab === 'theme'">
|
||||
<h3>{{ t('settings.tab.theme') }}</h3>
|
||||
<div class="theme-grid">
|
||||
@@ -350,6 +395,90 @@ input[type='number'] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clipart-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.clipart-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.clipart-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
background: var(--panel-alt);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.clipart-folder-name {
|
||||
font-variant-numeric: tabular-nums;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clipart-folder-empty {
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.clipart-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--accent);
|
||||
color: var(--accent-text, #fff);
|
||||
border: 1px solid transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary-btn:hover:not(:disabled) {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.primary-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ghost-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost-btn:hover {
|
||||
background: var(--panel-alt);
|
||||
}
|
||||
|
||||
.error-line {
|
||||
margin: 0 0 8px;
|
||||
color: var(--danger);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
Reference in new issue
Block a user