This commit is contained in:
tianmo committed 2026-11-07 15:12:13 +08:00
1 parent b3e9481fe5
commit e626e685fc
25 files changed
+189 -13

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ export function defaultConfig(): EmitterConfig {
previewUrl: '/images/star.png',
imageMode: 'fixed',
sequencePlayback: 'loop-forward',
sequenceStart: 'first',
sequenceFps: DEFAULT_SEQUENCE_FPS,
sequenceFrames: [],
weight: 100,
@@ -381,6 +382,7 @@ export function ensureEmitterConfig(config: EmitterConfig): EmitterConfig {
else if (!['once-hide', 'once-hold', 'loop-forward', 'loop-reverse'].includes(resource.sequencePlayback)) {
resource.sequencePlayback = 'loop-forward'
}
if (resource.sequenceStart !== 'random') resource.sequenceStart = 'first'
if (!Number.isFinite(resource.sequenceFps)) resource.sequenceFps = DEFAULT_SEQUENCE_FPS
resource.sequenceFps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || DEFAULT_SEQUENCE_FPS))
if (!Array.isArray(resource.sequenceFrames)) resource.sequenceFrames = []
+6 -3
View File
@@ -1,7 +1,7 @@
import { Container, Sprite, Graphics } from 'pixi.js'
import { pathDistributionProgress, sampleEmitterShape, sampleEmissionVelocityAngle, type EmitterShapeSample } from './emitterShape'
import { TrailController, createTrailState, resetTrailState, type ParticleTrailState } from './particleTrail'
import { pickImageResource, pickTrailResource, imageFrameState, imageFrameTexture } from './particleResources'
import { pickImageResource, imageSequenceStartOffset, pickTrailResource, imageFrameState, imageFrameTexture } from './particleResources'
import type { SceneCollider, EmitterConfig, EmitMode, ParticleState, ParticleInheritAttribute } from './particleTypes'
import { defaultConfig, ensureEmitterConfig } from './particleConfig'
@@ -48,6 +48,7 @@ interface Particle {
currentScaleX: number
currentScaleY: number
imageFrameIndex: number
sequenceStartOffset: number
collisionPaused: boolean
/** 当前粒子累计发生的碰撞次数。 */
collisionCount: number
@@ -188,6 +189,7 @@ export class ParticleEmitter extends Container {
currentScaleX: 1,
currentScaleY: 1,
imageFrameIndex: 0,
sequenceStartOffset: 0,
collisionPaused: false,
collisionCount: 0,
eventDepth: 0,
@@ -427,7 +429,7 @@ export class ParticleEmitter extends Container {
const s = p.sprite
const resource = cfg.imageResources.find((item) => item.id === p.resourceId)
if (resource) {
const frameState = imageFrameState(resource, p.maxLife - p.life)
const frameState = imageFrameState(resource, p.maxLife - p.life, p.sequenceStartOffset)
p.imageFrameIndex = frameState.index
s.texture = imageFrameTexture(resource, cfg.texture, p.imageFrameIndex)
s.visible = frameState.visible
@@ -729,6 +731,7 @@ export class ParticleEmitter extends Container {
? lerp(cfg.scaleYOverLifeMin, cfg.scaleYOverLifeMax, this.rng())
: cfg.scaleYOverLifeMin
p.resourceId = resource.id
p.sequenceStartOffset = imageSequenceStartOffset(resource, cfg.seed, spawnSerial)
p.colorSample = resource.colorMode === 'random'
? gradientColorAt(resource.colorGradient, seededUnit(cfg.seed, spawnSerial, 0x9e3779b9))
: hexToNumber(resource.colorStart)
@@ -754,7 +757,7 @@ export class ParticleEmitter extends Container {
}
p.currentScaleX = p.scaleStartX
p.currentScaleY = p.scaleStartY
const initialFrame = imageFrameState(resource, 0)
const initialFrame = imageFrameState(resource, 0, p.sequenceStartOffset)
p.imageFrameIndex = initialFrame.index
p.sprite.texture = imageFrameTexture(resource, cfg.texture, initialFrame.index)
p.sprite.anchor.set(resource.anchorX, resource.anchorY)
+14 -3
View File
@@ -1,5 +1,6 @@
import type { Texture } from 'pixi.js'
import type { EmitterConfig, ParticleImageResource, TrailImageResource } from './particleTypes'
import { seededUnit } from './particleMath'
export const DEFAULT_SEQUENCE_FRAME_DIRECTORY = '/images/frame-sequence/yanhua'
export const DEFAULT_SEQUENCE_FPS = 15
@@ -38,7 +39,7 @@ export function ensureDefaultSequenceFrames(resource: SequenceResourceTarget) {
export function defaultImageResource(texture: Texture<any> | null): ParticleImageResource {
return {
id: 1, texture, textureName: 'star', textureFolder: '',
previewUrl: '/images/star.png', imageMode: 'fixed', sequencePlayback: 'loop-forward', sequenceFps: DEFAULT_SEQUENCE_FPS, sequenceFrames: [],
previewUrl: '/images/star.png', imageMode: 'fixed', sequencePlayback: 'loop-forward', sequenceStart: 'first', sequenceFps: DEFAULT_SEQUENCE_FPS, sequenceFrames: [],
weight: 100, locked: false, scale: 1, anchorX: 0.5, anchorY: 0.5,
colorMode: 'fixed', colorStart: '#ffffff', colorEnd: '#ff4d4d', blend: 'normal',
colorGradient: [{ position: 0, color: '#ffffff' }, { position: 1, color: '#ff4d4d' }],
@@ -94,11 +95,21 @@ export function pickTrailResource(cfg: EmitterConfig, sample: number): TrailImag
return resources[resources.length - 1]
}
/** 随机起始帧使用独立采样,不改变粒子属性原有的随机序列。 */
export function imageSequenceStartOffset(
resource: Pick<ParticleImageResource, 'imageMode' | 'sequenceStart' | 'sequenceFrames'>,
seed: number,
spawnId: number,
) {
if (resource.imageMode !== 'sequence' || resource.sequenceStart !== 'random') return 0
return Math.floor(seededUnit(seed, spawnId, 0x6c8e9cf5) * resource.sequenceFrames.length)
}
/** 序列帧在粒子年龄处的帧号与可见性。 */
export function imageFrameState(resource: ParticleImageResource, age: number) {
export function imageFrameState(resource: ParticleImageResource, age: number, startOffset = 0) {
if (resource.imageMode !== 'sequence' || resource.sequenceFrames.length <= 1) return { index: 0, visible: true }
const fps = Math.min(120, Math.max(1, Number(resource.sequenceFps) || DEFAULT_SEQUENCE_FPS))
const rawIndex = Math.max(0, Math.floor(Math.max(0, age) * fps))
const rawIndex = Math.max(0, Math.floor(Math.max(0, age) * fps)) + Math.max(0, Math.floor(startOffset))
const lastIndex = resource.sequenceFrames.length - 1
if (resource.sequencePlayback === 'once-hide') {
return { index: Math.min(lastIndex, rawIndex), visible: rawIndex <= lastIndex }
+2
View File
@@ -73,6 +73,8 @@ export interface ParticleImageResource {
imageMode: 'fixed' | 'sequence'
/** 序列帧播放方式:单次隐藏、单次定格、正向循环或反向循环。 */
sequencePlayback: 'once-hide' | 'once-hold' | 'loop-forward' | 'loop-reverse'
/** 序列帧起始位置:固定首帧或按粒子出生随机。 */
sequenceStart: 'first' | 'random'
/** 序列帧每秒播放帧数。 */
sequenceFps: number
sequenceFrames: ParticleSequenceFrame[]
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "Vorwärts-Schleife",
"loopReverse": "Rückwärts-Schleife",
"playbackFps": "Bildrate der Wiedergabe",
"startFrame": "Startbild",
"fixedFirstFrame": "Erstes Bild fixiert",
"randomFrame": "Zufällig",
"chooseSequenceMulti": "Sequenzbilder auswählen (Mehrfachauswahl)",
"weight": "Gewicht %",
"unlockWeight": "Proportionssperre entsperren",
@@ -772,6 +775,11 @@
"tabsLabel": "Hilfebereiche",
"updates": "Versionshinweise",
"updatesSubtitle": "Aktuelle Funktionsupdates anzeigen",
"releaseSummaryV11106": "V1.11.06 aktualisiert",
"releaseHeadingV11106": "Update vom 06.11.2026:",
"releaseMenuLayerFix": "Die Anzeigeebene des Ziehpunkts im Menüsteuerungsbereich wurde korrigiert.",
"releaseSequenceStart": "Startbildsteuerung für Bildsequenzen hinzugefügt: festes erstes Bild oder zufälliger Start.",
"releaseSaveDialog": "Der Dialog zum Speichern von Konfigurationen wurde angepasst und zeigt nun vorhandene Konfigurationen an.",
"releaseSummaryV11105": "V1.11.05 aktualisiert",
"releaseHeadingV11105": "Update vom 05.11.2026:",
"releaseWindowFix": "Die Fensteranzeige wurde korrigiert.",
+8
View File
@@ -246,6 +246,9 @@
"loopForward": "Loop forward",
"loopReverse": "Loop reverse",
"playbackFps": "Playback FPS",
"startFrame": "Start frame",
"fixedFirstFrame": "Fixed first frame",
"randomFrame": "Random",
"chooseSequenceMulti": "Choose sequence images (multiple)",
"weight": "Weight %",
"unlockWeight": "Unlock weight",
@@ -795,6 +798,11 @@
"tabsLabel": "Help sections",
"updates": "Release Notes",
"updatesSubtitle": "View recent feature updates",
"releaseSummaryV11106": "Updated to V1.11.06",
"releaseHeadingV11106": "Updated 2026-11-06:",
"releaseMenuLayerFix": "Fixed the display layer of the drag button in the menu control area.",
"releaseSequenceStart": "Added start-frame control for image sequences, allowing a fixed first frame or a random start frame.",
"releaseSaveDialog": "Updated the save-configuration dialog and added the existing configuration list.",
"releaseSummaryV11105": "Updated to V1.11.05",
"releaseHeadingV11105": "Updated 2026-11-05:",
"releaseWindowFix": "Fixed window display.",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "Bucle hacia delante",
"loopReverse": "Bucle inverso",
"playbackFps": "Velocidad de fotogramas de reproducción",
"startFrame": "Fotograma inicial",
"fixedFirstFrame": "Primer fotograma fijo",
"randomFrame": "Aleatorio",
"chooseSequenceMulti": "Seleccionar fotogramas de secuencia (múltiples selecciones)",
"weight": "Peso %",
"unlockWeight": "Desbloquear bloqueo de proporción",
@@ -772,6 +775,11 @@
"tabsLabel": "Secciones de ayuda",
"updates": "Actualizaciones de versión",
"updatesSubtitle": "Ver actualizaciones recientes de funciones",
"releaseSummaryV11106": "Actualizado a V1.11.06",
"releaseHeadingV11106": "Actualización del 06/11/2026:",
"releaseMenuLayerFix": "Corregida la capa de visualización del botón de arrastre en el área de control del menú.",
"releaseSequenceStart": "Añadido el control del fotograma inicial en secuencias de imágenes, con primer fotograma fijo o inicio aleatorio.",
"releaseSaveDialog": "Ajustada la ventana para guardar configuraciones y añadida la lista de configuraciones existentes.",
"releaseSummaryV11105": "Actualizado a V1.11.05",
"releaseHeadingV11105": "Actualización del 05/11/2026:",
"releaseWindowFix": "Corregida la visualización de la ventana.",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "Boucle avant",
"loopReverse": "Boucle arrière",
"playbackFps": "Fréquence d'images de lecture",
"startFrame": "Image de départ",
"fixedFirstFrame": "Première image fixe",
"randomFrame": "Aléatoire",
"chooseSequenceMulti": "Sélectionner des images de séquence (sélections multiples)",
"weight": "Poids %",
"unlockWeight": "Déverrouiller le verrouillage des proportions",
@@ -772,6 +775,11 @@
"tabsLabel": "Sections de l’aide",
"updates": "Mises à jour",
"updatesSubtitle": "Voir les mises à jour récentes des fonctionnalités",
"releaseSummaryV11106": "Mis à jour vers V1.11.06",
"releaseHeadingV11106": "Mise à jour du 06/11/2026 :",
"releaseMenuLayerFix": "Correction de l’ordre d’affichage du bouton de glissement dans la zone de contrôle du menu.",
"releaseSequenceStart": "Ajout du contrôle de l’image de départ des séquences, avec première image fixe ou départ aléatoire.",
"releaseSaveDialog": "Mise à jour de la boîte d’enregistrement des configurations, avec affichage des configurations existantes.",
"releaseSummaryV11105": "Mis à jour vers V1.11.05",
"releaseHeadingV11105": "Mise à jour du 05/11/2026 :",
"releaseWindowFix": "Correction de l’affichage de la fenêtre.",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "正方向ループ",
"loopReverse": "逆方向ループ",
"playbackFps": "再生フレームレート",
"startFrame": "開始フレーム",
"fixedFirstFrame": "先頭フレームで固定",
"randomFrame": "ランダム",
"chooseSequenceMulti": "シーケンスフレームの選択(複数選択)",
"weight": "重み %",
"unlockWeight": "プロポーションロックを解除する",
@@ -772,6 +775,11 @@
"tabsLabel": "ヘルプのセクション",
"updates": "バージョン更新",
"updatesSubtitle": "最近の機能更新を見る",
"releaseSummaryV11106": "V1.11.06 更新済み",
"releaseHeadingV11106": "2026-11-06 更新:",
"releaseMenuLayerFix": "メニューコントロール領域のドラッグボタンの表示階層を修正しました。",
"releaseSequenceStart": "画像シーケンスモードに開始フレーム制御を追加し、先頭フレーム固定とランダム開始を選択できるようにしました。",
"releaseSaveDialog": "設定保存ダイアログの表示を調整し、既存の設定一覧も表示するようにしました。",
"releaseSummaryV11105": "V1.11.05 更新済み",
"releaseHeadingV11105": "2026-11-05 更新:",
"releaseWindowFix": "ウィンドウの表示を修正しました。",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "정방향 루프",
"loopReverse": "역방향 루프",
"playbackFps": "재생 프레임 속도",
"startFrame": "시작 프레임",
"fixedFirstFrame": "첫 프레임 고정",
"randomFrame": "무작위",
"chooseSequenceMulti": "시퀀스 프레임 선택(다중 선택)",
"weight": "가중치 %",
"unlockWeight": "비율 잠금 해제",
@@ -772,6 +775,11 @@
"tabsLabel": "도움말 섹션",
"updates": "버전 업데이트",
"updatesSubtitle": "최근 기능 업데이트 보기",
"releaseSummaryV11106": "V1.11.06 업데이트됨",
"releaseHeadingV11106": "2026-11-06 업데이트:",
"releaseMenuLayerFix": "메뉴 컨트롤 영역의 드래그 버튼 표시 레이어를 수정했습니다.",
"releaseSequenceStart": "이미지 시퀀스 모드에 시작 프레임 제어를 추가하여 첫 프레임 고정 또는 무작위 시작을 선택할 수 있습니다.",
"releaseSaveDialog": "설정 저장 대화상자 표시를 조정하고 기존 설정 목록도 표시하도록 했습니다.",
"releaseSummaryV11105": "V1.11.05 업데이트됨",
"releaseHeadingV11105": "2026-11-05 업데이트:",
"releaseWindowFix": "창 표시를 수정했습니다.",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "Loop para frente",
"loopReverse": "Loop reverso",
"playbackFps": "Taxa de quadros de reprodução",
"startFrame": "Quadro inicial",
"fixedFirstFrame": "Primeiro quadro fixo",
"randomFrame": "Aleatório",
"chooseSequenceMulti": "Selecione quadros de sequência (múltiplas seleções)",
"weight": "Peso %",
"unlockWeight": "Desbloquear bloqueio de proporção",
@@ -772,6 +775,11 @@
"tabsLabel": "Seções da ajuda",
"updates": "Atualizações da versão",
"updatesSubtitle": "Ver atualizações recentes de recursos",
"releaseSummaryV11106": "Atualizado para V1.11.06",
"releaseHeadingV11106": "Atualização de 06/11/2026:",
"releaseMenuLayerFix": "Corrigida a camada de exibição do botão de arrastar na área de controle do menu.",
"releaseSequenceStart": "Adicionado controle de quadro inicial para sequências de imagens, permitindo primeiro quadro fixo ou início aleatório.",
"releaseSaveDialog": "Ajustada a janela de salvar configuração e adicionada a lista de configurações existentes.",
"releaseSummaryV11105": "Atualizado para V1.11.05",
"releaseHeadingV11105": "Atualização de 05/11/2026:",
"releaseWindowFix": "Corrigida a exibição da janela.",
+8
View File
@@ -246,6 +246,9 @@
"loopForward": "Цикл в прямом направлении",
"loopReverse": "Цикл в обратном направлении",
"playbackFps": "Частота воспроизведения, кадр/с",
"startFrame": "Начальный кадр",
"fixedFirstFrame": "Фиксировать первый кадр",
"randomFrame": "Случайно",
"chooseSequenceMulti": "Выбрать кадры последовательности (несколько)",
"weight": "Вес, %",
"unlockWeight": "Разблокировать вес",
@@ -795,6 +798,11 @@
"tabsLabel": "Разделы справки",
"updates": "Обновления версии",
"updatesSubtitle": "Последние обновления функций",
"releaseSummaryV11106": "Обновлено до V1.11.06",
"releaseHeadingV11106": "Обновление от 06.11.2026:",
"releaseMenuLayerFix": "Исправлен порядок отображения кнопки перетаскивания в области управления меню.",
"releaseSequenceStart": "Добавлено управление начальным кадром последовательности изображений с выбором фиксированного первого или случайного начального кадра.",
"releaseSaveDialog": "Обновлено отображение окна сохранения конфигурации и добавлен список существующих конфигураций.",
"releaseSummaryV11105": "Обновлено до V1.11.05",
"releaseHeadingV11105": "Обновление от 05.11.2026:",
"releaseWindowFix": "Исправлено отображение окна.",
+8
View File
@@ -246,6 +246,9 @@
"loopForward": "正向循环",
"loopReverse": "反向循环",
"playbackFps": "播放帧率",
"startFrame": "起始序列帧",
"fixedFirstFrame": "固定首帧",
"randomFrame": "随机",
"chooseSequenceMulti": "选择序列帧(多选)",
"weight": "占比%",
"unlockWeight": "解除占比锁定",
@@ -795,6 +798,11 @@
"tabsLabel": "使用说明栏目",
"updates": "版本更新",
"updatesSubtitle": "查看近期功能更新记录",
"releaseSummaryV11106": "V1.11.06 已更新",
"releaseHeadingV11106": "2026-11-06更新:",
"releaseMenuLayerFix": "修正菜单控制区拖动按钮显示层级",
"releaseSequenceStart": "加入图片序列帧模式下的起始帧控制,允许固定首帧图片与随机起始帧",
"releaseSaveDialog": "保存配置弹窗显示调整,同时显示已有配置",
"releaseSummaryV11105": "V1.11.05 已更新",
"releaseHeadingV11105": "2026-11-05更新:",
"releaseWindowFix": "修复窗口显示",
+8
View File
@@ -234,6 +234,9 @@
"loopForward": "正向循環",
"loopReverse": "反向循環",
"playbackFps": "播放影格率",
"startFrame": "起始序列幀",
"fixedFirstFrame": "固定首幀",
"randomFrame": "隨機",
"chooseSequenceMulti": "選擇序列幀(多選)",
"weight": "權重 %",
"unlockWeight": "解除佔比鎖定",
@@ -772,6 +775,11 @@
"tabsLabel": "使用說明欄目",
"updates": "版本更新",
"updatesSubtitle": "查看近期功能更新記錄",
"releaseSummaryV11106": "V1.11.06 已更新",
"releaseHeadingV11106": "2026-11-06更新:",
"releaseMenuLayerFix": "修正選單控制區拖曳按鈕顯示層級",
"releaseSequenceStart": "加入圖片序列幀模式下的起始幀控制,允許固定首幀圖片與隨機起始幀",
"releaseSaveDialog": "調整儲存設定對話框顯示,同時顯示現有設定",
"releaseSummaryV11105": "V1.11.05 已更新",
"releaseHeadingV11105": "2026-11-05更新:",
"releaseWindowFix": "修正視窗顯示",
+1 -1
View File
@@ -286,7 +286,7 @@ function onTimelineDrag(e: PointerEvent) {
box-sizing: border-box;
}
.timeline-collapse-toggle {
position: absolute; left: 50%; bottom: calc(var(--operation-bar-rendered-height, 84px) - 1px); z-index: 21;
position: absolute; left: 50%; bottom: calc(var(--operation-bar-rendered-height, 84px) - 1px); z-index: 19;
width: 96px; height: 20.4px; padding: 0; transform: translateX(-50%);
border: 0; background: transparent; color: #8792a6; cursor: ns-resize;
touch-action: none; user-select: none;
+6
View File
@@ -255,6 +255,12 @@ const activeTab = ref<'guide' | 'updates'>('guide')
const openModules = ref<boolean[]>(Array.from({ length: 9 }, () => false))
const releases = [
{
date: '2026-11-06',
title: 'help.releaseSummaryV11106',
heading: 'help.releaseHeadingV11106',
updates: ['help.releaseMenuLayerFix', 'help.releaseSequenceStart', 'help.releaseSaveDialog'],
},
{
date: '2026-11-05',
title: 'help.releaseSummaryV11105',
+7
View File
@@ -31,6 +31,12 @@
</select>
</label>
<label class="row"><span>{{ $t('image.playbackFps') }}</span><input v-model.number="resource.sequenceFps" type="number" min="1" max="120" step="1" class="inp" /></label>
<label class="row"><span>{{ $t('image.startFrame') }}</span>
<select v-model="resource.sequenceStart" class="inp">
<option value="first">{{ $t('image.fixedFirstFrame') }}</option>
<option value="random">{{ $t('image.randomFrame') }}</option>
</select>
</label>
<button class="sequence-pick" data-matomo-event="resources.pick-sequence" @click="$emit('pickSequence', resource.id)">{{ $t('image.chooseSequenceMulti') }}</button>
<div class="sequence-note">{{ sequenceOrderText }}</div>
</template>
@@ -123,6 +129,7 @@ type CommonImageResource = {
previewUrl: string
imageMode?: 'fixed' | 'sequence'
sequencePlayback?: 'once-hide' | 'once-hold' | 'loop-forward' | 'loop-reverse'
sequenceStart?: 'first' | 'random'
sequenceFps?: number
sequenceFrames?: Array<{ name: string; previewUrl: string }>
textureName: string
@@ -69,6 +69,24 @@
autofocus
/>
</label>
<div class="project-save-existing">
<div class="project-save-existing-title">{{ $t('config.local.listTitle') }}</div>
<div v-if="localProjects.length" class="project-save-existing-list">
<button
v-for="project in localProjects"
:key="project.name"
type="button"
class="project-save-existing-row"
:class="{ selected: projectNameDraft === project.name }"
:aria-pressed="projectNameDraft === project.name"
@click="projectNameDraft = project.name; projectSaveError = ''"
>
<span>{{ project.name }}</span>
<small v-if="store.settings.showProjectSize">{{ formatProjectSize(project.size) }}</small>
</button>
</div>
<div v-else class="project-save-existing-empty">{{ $t('config.local.empty') }}</div>
</div>
<div v-if="projectSaveError" class="project-save-error">{{ projectSaveError }}</div>
<div class="project-save-actions">
<button type="button" :disabled="projectStorageBusy" @click="closeProjectSaveDialog">{{ $t('common.cancel') }}</button>
@@ -168,7 +186,30 @@ function presetDisplayName(preset: PresetOption) {
.project-save-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 13px; font-size: 14px; font-weight: 700; }
.project-save-head button { width: 24px; height: 24px; border: 1px solid #3b4864; border-radius: 5px; background: #252f43; color: #bfc9dc; cursor: pointer; }
.project-save-dialog label { display: flex; flex-direction: column; gap: 6px; color: #8f9cb8; font-size: 11px; }
.project-save-dialog .inp { width: 100%; height: 34px; box-sizing: border-box; }
.project-save-dialog .inp {
width: 100%; height: 34px; box-sizing: border-box; padding: 0 8px;
border: 1px solid #2e2e44; border-radius: 5px; outline: none;
background: #1a1a28; color: #dde; font: inherit; font-size: 12px;
}
.project-save-dialog .inp:focus { border-color: #716eff; }
.project-save-existing { margin-top: 13px; padding: 8px; border: 1px solid #2c3850; border-radius: 7px; background: #111725; }
.project-save-existing-title { margin-bottom: 6px; color: #aeb9cf; font-size: 11px; font-weight: 700; }
.project-save-existing-list {
display: grid; gap: 2px; max-height: 164px; overflow-y: auto;
scrollbar-width: thin; scrollbar-color: #3a455e transparent;
}
.project-save-existing-list::-webkit-scrollbar { width: 6px; }
.project-save-existing-list::-webkit-scrollbar-thumb { border-radius: 3px; background: #3a455e; }
.project-save-existing-row {
display: flex; width: 100%; min-height: 30px; align-items: center; gap: 8px; padding: 0 8px;
border: 0; border-radius: 5px; background: transparent; color: #c7d2e5; font: inherit; font-size: 11px; text-align: left; cursor: pointer;
}
.project-save-existing-row:hover { background: #1d273a; color: #fff; }
.project-save-existing-row.selected { background: #25264b; color: #fff; box-shadow: inset 2px 0 #8179ff; }
.project-save-existing-row:focus-visible { outline: 1px solid #8179ff; outline-offset: -1px; }
.project-save-existing-row span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.project-save-existing-row small { flex: 0 0 auto; color: #7f8aa6; font-size: 9px; }
.project-save-existing-empty { padding: 5px 2px; color: #68758d; font-size: 10px; text-align: center; }
.project-save-error { margin-top: 8px; color: #ef9aa7; font-size: 10px; line-height: 1.4; }
.project-save-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; }
.project-save-actions button { height: 31px; padding: 0 14px; border: 1px solid #3d4b66; border-radius: 6px; background: #27334a; color: #cbd5e7; cursor: pointer; }
@@ -228,6 +228,7 @@ function addImageResource() {
previewUrl: '/images/star.png',
imageMode: 'fixed',
sequencePlayback: 'loop-forward',
sequenceStart: 'first',
sequenceFps: DEFAULT_SEQUENCE_FPS,
sequenceFrames: [],
weight: 100,
@@ -333,6 +334,7 @@ function resetTexture(resourceId: number) {
resource.previewUrl = '/images/star.png'
resource.imageMode = 'fixed'
resource.sequencePlayback = 'loop-forward'
resource.sequenceStart = 'first'
resource.sequenceFps = DEFAULT_SEQUENCE_FPS
resource.sequenceFrames = []
resource.scale = 1