0.8.1: dirty-state + Cmd/Ctrl+S, fullscreen plays to OUT, UX fixes
- Unsaved-changes indicator: a small accent dot appears on the save
button whenever the scene has changed since the last save/load.
Save/load/new all mark the project clean on next tick.
- Cmd/Ctrl+S saves the project (reuses the same flow as the button;
prompts for a name on first save). Tooltip now mentions the
shortcut.
- Fullscreen preview plays to the OUT marker (or scene.duration when
no OUT is set) instead of looping at the last keyframe. Dropped
the runtime's effectiveEnd helper; RUNTIME_VERSION 13 → 14.
- "Still frame" keyframes no longer disable easing. A held kf's
easing still drives the segment INTO it from the previous kf, so
you can ease into a pause. Only the first kf disables easing
(nothing precedes it). Dropped `timeline.holdDisabledHint`.
- Settings dialog: hint text 11px → 13px (and dropped italic) so
the inline help is readable; section headings 10px → 13px.
- Canvas stage shadow: `shadowCfg` always emits explicit values plus
a `shadowEnabled` flag instead of returning `{}`. Konva's default
shadowOpacity is 1, so bailing with `{}` could leave stale shadow
attrs on the shape. Preview and editor now gate on the same
condition deterministically.
- Sidebar empty-state copy: "No clips" → "No clips yet" to match
"No buttons yet".
Regression tests: runtime rangeOut fallback is scene.duration (not
last kf); sidebar empty-states both contain "yet"; the
holdDisabledHint key is gone; saveProject tooltips mention the
shortcut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1e89ea86e0
commit
82ee5c98d0
10 files changed
+133
-48
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "animatr",
|
"name": "animatr",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+59
-4
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
PhArrowClockwise,
|
PhArrowClockwise,
|
||||||
PhArrowCounterClockwise,
|
PhArrowCounterClockwise,
|
||||||
@@ -132,12 +132,29 @@ const loadError = ref<string | null>(null)
|
|||||||
const savedToast = ref<string | null>(null)
|
const savedToast = ref<string | null>(null)
|
||||||
const currentProjectId = ref<string | null>(null)
|
const currentProjectId = ref<string | null>(null)
|
||||||
const currentProjectName = ref<string | null>(null)
|
const currentProjectName = ref<string | null>(null)
|
||||||
|
// Dirty = scene has changed since the last save/load. Deep-watches
|
||||||
|
// store.scene; save/load paths clear it after the next tick so the
|
||||||
|
// replacement itself doesn't leave the project flagged as unsaved.
|
||||||
|
const isDirty = ref(false)
|
||||||
|
|
||||||
usePlaybackLoop()
|
usePlaybackLoop()
|
||||||
const store = useSceneStore()
|
const store = useSceneStore()
|
||||||
const clipart = useClipartStore()
|
const clipart = useClipartStore()
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.scene,
|
||||||
|
() => {
|
||||||
|
isDirty.value = true
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function markClean() {
|
||||||
|
await nextTick()
|
||||||
|
isDirty.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function rewind() {
|
function rewind() {
|
||||||
store.setPlayhead(store.playbackIn)
|
store.setPlayhead(store.playbackIn)
|
||||||
}
|
}
|
||||||
@@ -168,6 +185,7 @@ async function saveProject() {
|
|||||||
currentProjectName.value = rec.name
|
currentProjectName.value = rec.name
|
||||||
setLastOpenedId(rec.id)
|
setLastOpenedId(rec.id)
|
||||||
showSavedToast(t('app.savedToast', { name: rec.name }))
|
showSavedToast(t('app.savedToast', { name: rec.name }))
|
||||||
|
await markClean()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renameProject() {
|
async function renameProject() {
|
||||||
@@ -225,6 +243,7 @@ async function importFromFile() {
|
|||||||
currentProjectId.value = null
|
currentProjectId.value = null
|
||||||
currentProjectName.value = file.name.replace(/\.[^.]+$/, '')
|
currentProjectName.value = file.name.replace(/\.[^.]+$/, '')
|
||||||
loadError.value = null
|
loadError.value = null
|
||||||
|
await markClean()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg =
|
const msg =
|
||||||
err instanceof ProjectFileError ? err.message : (err as Error).message || String(err)
|
err instanceof ProjectFileError ? err.message : (err as Error).message || String(err)
|
||||||
@@ -235,7 +254,7 @@ async function importFromFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openFromRecent(project: StoredProject) {
|
async function openFromRecent(project: StoredProject) {
|
||||||
const cloned = JSON.parse(JSON.stringify(project.scene))
|
const cloned = JSON.parse(JSON.stringify(project.scene))
|
||||||
store.loadScene(cloned)
|
store.loadScene(cloned)
|
||||||
history.resetTo(cloned)
|
history.resetTo(cloned)
|
||||||
@@ -243,15 +262,17 @@ function openFromRecent(project: StoredProject) {
|
|||||||
currentProjectName.value = project.name
|
currentProjectName.value = project.name
|
||||||
setLastOpenedId(project.id)
|
setLastOpenedId(project.id)
|
||||||
loadError.value = null
|
loadError.value = null
|
||||||
|
await markClean()
|
||||||
}
|
}
|
||||||
|
|
||||||
function startNewProject() {
|
async function startNewProject() {
|
||||||
const blank = createBlankScene()
|
const blank = createBlankScene()
|
||||||
store.loadScene(blank)
|
store.loadScene(blank)
|
||||||
history.resetTo(blank)
|
history.resetTo(blank)
|
||||||
currentProjectId.value = null
|
currentProjectId.value = null
|
||||||
currentProjectName.value = null
|
currentProjectName.value = null
|
||||||
loadError.value = null
|
loadError.value = null
|
||||||
|
await markClean()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSavedToast(msg: string) {
|
function showSavedToast(msg: string) {
|
||||||
@@ -308,6 +329,11 @@ function onKeyDown(e: KeyboardEvent) {
|
|||||||
history.redo()
|
history.redo()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (key === 's' && !e.shiftKey && !e.altKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
void saveProject()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!isEditable(e.target) && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
if (!isEditable(e.target) && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
|
||||||
const key = e.key.toLowerCase()
|
const key = e.key.toLowerCase()
|
||||||
@@ -453,8 +479,14 @@ onBeforeUnmount(() => {
|
|||||||
<button class="icon-btn" :title="t('app.recentProjects')" @click="welcomeOpen = true">
|
<button class="icon-btn" :title="t('app.recentProjects')" @click="welcomeOpen = true">
|
||||||
<PhFolderOpen :size="16" />
|
<PhFolderOpen :size="16" />
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" :title="t('app.saveProject')" @click="saveProject">
|
<button
|
||||||
|
class="icon-btn save-btn"
|
||||||
|
:class="{ dirty: isDirty }"
|
||||||
|
:title="isDirty ? t('app.saveProjectDirty') : t('app.saveProject')"
|
||||||
|
@click="saveProject"
|
||||||
|
>
|
||||||
<PhFloppyDisk :size="16" />
|
<PhFloppyDisk :size="16" />
|
||||||
|
<span v-if="isDirty" class="dirty-dot" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-btn" :title="t('app.documentSettings')" @click="settingsOpen = true">
|
<button class="icon-btn" :title="t('app.documentSettings')" @click="settingsOpen = true">
|
||||||
<PhGearSix :size="16" />
|
<PhGearSix :size="16" />
|
||||||
@@ -656,6 +688,29 @@ onBeforeUnmount(() => {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Unsaved-changes indicator on the save button. A small accent dot
|
||||||
|
pinned to the top-right of the icon, visible at a glance. `position:
|
||||||
|
relative` on the button lets the dot anchor inside it. */
|
||||||
|
.save-btn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn.dirty {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dirty-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 0 2px var(--panel);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
grid-area: side;
|
grid-area: side;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
|
|||||||
@@ -840,11 +840,14 @@ const stackItems = computed<StackItem[]>(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function shadowCfg(props: AnimatableProps): Record<string, unknown> {
|
function shadowCfg(props: AnimatableProps): Record<string, unknown> {
|
||||||
|
// Konva's default shadowOpacity is 1, so returning {} for the "no
|
||||||
|
// shadow" case isn't enough — the shape would reuse whatever values
|
||||||
|
// were set on a previous render. Always emit explicit values and let
|
||||||
|
// `shadowEnabled` be the single source of truth. The gate matches
|
||||||
|
// the canvas/preview path: opacity > 0 AND some blur or distance.
|
||||||
const opacity = props.shadowOpacity ?? 0
|
const opacity = props.shadowOpacity ?? 0
|
||||||
if (opacity <= 0) return {}
|
|
||||||
const blur = props.shadowBlur ?? 0
|
const blur = props.shadowBlur ?? 0
|
||||||
const distance = props.shadowDistance ?? 0
|
const distance = props.shadowDistance ?? 0
|
||||||
if (blur <= 0 && distance <= 0) return {}
|
|
||||||
const angle = props.shadowAngle ?? 90
|
const angle = props.shadowAngle ?? 90
|
||||||
const rad = (angle * Math.PI) / 180
|
const rad = (angle * Math.PI) / 180
|
||||||
return {
|
return {
|
||||||
@@ -853,6 +856,7 @@ function shadowCfg(props: AnimatableProps): Record<string, unknown> {
|
|||||||
shadowOpacity: opacity,
|
shadowOpacity: opacity,
|
||||||
shadowOffsetX: Math.cos(rad) * distance,
|
shadowOffsetX: Math.cos(rad) * distance,
|
||||||
shadowOffsetY: Math.sin(rad) * distance,
|
shadowOffsetY: Math.sin(rad) * distance,
|
||||||
|
shadowEnabled: opacity > 0 && (blur > 0 || distance > 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -454,11 +454,11 @@ section {
|
|||||||
|
|
||||||
section h3 {
|
section h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 10px;
|
font-size: 13px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim-strong);
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
@@ -558,9 +558,9 @@ input[type='number'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
color: var(--text-dim);
|
line-height: 1.45;
|
||||||
font-style: italic;
|
color: var(--text-dim-strong);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2825,13 +2825,13 @@ onBeforeUnmount(() => {
|
|||||||
v-for="opt in EASING_OPTIONS"
|
v-for="opt in EASING_OPTIONS"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
class="kf-menu-item"
|
class="kf-menu-item"
|
||||||
:class="{ active: menu.currentEasing === opt.value && !menu.isHeld }"
|
:class="{ active: menu.currentEasing === opt.value }"
|
||||||
:disabled="menu.isFirst || menu.isHeld"
|
:disabled="menu.isFirst"
|
||||||
@click="applyEasing(opt.value)"
|
@click="applyEasing(opt.value)"
|
||||||
>
|
>
|
||||||
<span class="kf-menu-icon">
|
<span class="kf-menu-icon">
|
||||||
<PhCheck
|
<PhCheck
|
||||||
v-if="menu.currentEasing === opt.value && !menu.isHeld"
|
v-if="menu.currentEasing === opt.value"
|
||||||
:size="12"
|
:size="12"
|
||||||
weight="bold"
|
weight="bold"
|
||||||
/>
|
/>
|
||||||
@@ -2839,7 +2839,6 @@ onBeforeUnmount(() => {
|
|||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
</button>
|
</button>
|
||||||
<div v-if="menu.isFirst" class="kf-menu-note">{{ t('timeline.firstKfHint') }}</div>
|
<div v-if="menu.isFirst" class="kf-menu-note">{{ t('timeline.firstKfHint') }}</div>
|
||||||
<div v-else-if="menu.isHeld" class="kf-menu-note">{{ t('timeline.holdDisabledHint') }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="kf-menu-divider"></div>
|
<div class="kf-menu-divider"></div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -685,6 +685,20 @@ export function registerAllTests(): void {
|
|||||||
expect(r).toContain('shadowColor')
|
expect(r).toContain('shadowColor')
|
||||||
expect(r).toContain('out.blur')
|
expect(r).toContain('out.blur')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('runtime rangeOut falls back to scene.duration (not last keyframe)', () => {
|
||||||
|
// Regression: the runtime used to fall back to `effectiveEnd(scene)`
|
||||||
|
// — the time of the last keyframe — when scene.outTime was unset.
|
||||||
|
// That made fullscreen preview loop at the last kf, ignoring any
|
||||||
|
// blank tail before the OUT mark and any still-frame-past-end
|
||||||
|
// extension. The fallback is now scene.duration, matching the
|
||||||
|
// editor's `playbackOut` getter.
|
||||||
|
const r = generateRuntimeScript()
|
||||||
|
// The old helper must be gone entirely.
|
||||||
|
expect(r.indexOf('effectiveEnd')).toBe(-1)
|
||||||
|
// And the current fallback must be scene.duration.
|
||||||
|
expect(r).toContain('scene.outTime==null?scene.duration:scene.outTime')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('playback — transport', () => {
|
suite('playback — transport', () => {
|
||||||
@@ -4350,6 +4364,35 @@ export function registerAllTests(): void {
|
|||||||
expect(title === 'timeline.renameScriptTrackDialog.title').toBe(false)
|
expect(title === 'timeline.renameScriptTrackDialog.title').toBe(false)
|
||||||
expect(confirm === 'timeline.renameScriptTrackDialog.confirm').toBe(false)
|
expect(confirm === 'timeline.renameScriptTrackDialog.confirm').toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('sidebar noClips / noButtons empty-states both suggest "yet"', () => {
|
||||||
|
// Regression: the clip library used to read "No clips" while the
|
||||||
|
// button library read "No buttons yet". Minor inconsistency that
|
||||||
|
// the user called out — both should suggest "more coming", not
|
||||||
|
// "there will never be any".
|
||||||
|
expect(i18nT('sidebar.noClips')).toContain('yet')
|
||||||
|
expect(i18nT('sidebar.noButtons')).toContain('yet')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('holdDisabledHint key is gone (held kfs keep easing now)', () => {
|
||||||
|
// Regression: "Still frame — easing disabled" used to appear in
|
||||||
|
// the keyframe context menu and the easing buttons were grayed
|
||||||
|
// out. A held kf's easing still matters for the segment INTO it
|
||||||
|
// from the previous keyframe, so the restriction was wrong.
|
||||||
|
// Pin that the note is no longer emitted.
|
||||||
|
const v = i18nT('timeline.holdDisabledHint')
|
||||||
|
expect(v === 'timeline.holdDisabledHint').toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('app.saveProject tooltip mentions the Ctrl/Cmd+S shortcut', () => {
|
||||||
|
// Regression: Cmd/Ctrl+S was added as a keyboard save shortcut
|
||||||
|
// alongside the dirty-state dot. Surface it in the tooltip so
|
||||||
|
// the shortcut is discoverable; assert both the idle and the
|
||||||
|
// unsaved-changes variant stay in sync with the handler.
|
||||||
|
expect(i18nT('app.saveProject')).toContain('Ctrl/Cmd+S')
|
||||||
|
expect(i18nT('app.saveProjectDirty')).toContain('Ctrl/Cmd+S')
|
||||||
|
expect(i18nT('app.saveProjectDirty')).toContain('unsaved')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('timeline — scrub-handle obscured logic', () => {
|
suite('timeline — scrub-handle obscured logic', () => {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const en = {
|
|||||||
play: 'Play (Space)',
|
play: 'Play (Space)',
|
||||||
stop: 'Stop',
|
stop: 'Stop',
|
||||||
recentProjects: 'Recent projects',
|
recentProjects: 'Recent projects',
|
||||||
saveProject: 'Save project (browser storage)',
|
saveProject: 'Save project (Ctrl/Cmd+S)',
|
||||||
|
saveProjectDirty: 'Save project (unsaved changes, Ctrl/Cmd+S)',
|
||||||
documentSettings: 'Document settings',
|
documentSettings: 'Document settings',
|
||||||
export: 'Export',
|
export: 'Export',
|
||||||
mainTimeline: 'Main timeline',
|
mainTimeline: 'Main timeline',
|
||||||
@@ -66,7 +67,7 @@ export const en = {
|
|||||||
clipLayerCount: '{n} layers',
|
clipLayerCount: '{n} layers',
|
||||||
renameTooltip: 'Rename',
|
renameTooltip: 'Rename',
|
||||||
deleteTooltip: 'Delete',
|
deleteTooltip: 'Delete',
|
||||||
noClips: 'No clips',
|
noClips: 'No clips yet',
|
||||||
shape: {
|
shape: {
|
||||||
rect: 'Rect',
|
rect: 'Rect',
|
||||||
circle: 'Circle',
|
circle: 'Circle',
|
||||||
@@ -243,7 +244,6 @@ export const en = {
|
|||||||
easingEaseOut: 'Ease Out',
|
easingEaseOut: 'Ease Out',
|
||||||
easingEaseInOut: 'Ease In-Out',
|
easingEaseInOut: 'Ease In-Out',
|
||||||
firstKfHint: 'First keyframe — easing has no effect',
|
firstKfHint: 'First keyframe — easing has no effect',
|
||||||
holdDisabledHint: 'Still frame — easing disabled',
|
|
||||||
stillFrame: 'Still frame',
|
stillFrame: 'Still frame',
|
||||||
duplicate: 'Duplicate',
|
duplicate: 'Duplicate',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
|
|||||||
+7
-26
@@ -1,6 +1,6 @@
|
|||||||
import type { Scene } from '@/types/scene'
|
import type { Scene } from '@/types/scene'
|
||||||
|
|
||||||
const RUNTIME_VERSION = '13'
|
const RUNTIME_VERSION = '14'
|
||||||
|
|
||||||
const ENGINE_SOURCE = `(function(){
|
const ENGINE_SOURCE = `(function(){
|
||||||
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
if (window.AnimatrPlayer && window.AnimatrPlayer.__v === '${RUNTIME_VERSION}') return;
|
||||||
@@ -128,36 +128,17 @@ const ENGINE_SOURCE = `(function(){
|
|||||||
})).then(function(){return imgs;});
|
})).then(function(){return imgs;});
|
||||||
}
|
}
|
||||||
|
|
||||||
function effectiveEnd(scene){
|
|
||||||
var max=0;
|
|
||||||
for(var i=0;i<scene.layers.length;i++){
|
|
||||||
var kfs=scene.layers[i].keyframes;
|
|
||||||
for(var j=0;j<kfs.length;j++){ if(kfs[j].time>max) max=kfs[j].time; }
|
|
||||||
}
|
|
||||||
var instances=scene.clipInstances||[];
|
|
||||||
var clips=scene.clips||[];
|
|
||||||
var clipById={};
|
|
||||||
for(var ci=0;ci<clips.length;ci++) clipById[clips[ci].id]=clips[ci];
|
|
||||||
for(var ii=0;ii<instances.length;ii++){
|
|
||||||
var inst=instances[ii];
|
|
||||||
var cl=clipById[inst.clipId];
|
|
||||||
if(!cl) continue;
|
|
||||||
var trimS=inst.trimStart||0;
|
|
||||||
var trimE=inst.trimEnd||0;
|
|
||||||
var end=inst.startTime+Math.max(0,cl.duration-trimS-trimE);
|
|
||||||
if(end>max) max=end;
|
|
||||||
}
|
|
||||||
if(max<=0) return scene.duration;
|
|
||||||
return Math.min(max,scene.duration);
|
|
||||||
}
|
|
||||||
function mount(container,scene,opts){
|
function mount(container,scene,opts){
|
||||||
if(!container)return;
|
if(!container)return;
|
||||||
opts=opts||{};
|
opts=opts||{};
|
||||||
var loop=opts.loop!==false;
|
var loop=opts.loop!==false;
|
||||||
// Honour explicit in/out points; fall back to 0 and the animation's own end.
|
// Honour explicit in/out points. When outTime isn't set, play the
|
||||||
|
// full timeline (scene.duration) — matches the editor's playbackOut
|
||||||
|
// getter. Using the last keyframe as a fallback made still-frame
|
||||||
|
// endings and trailing silence (intentionally empty space before
|
||||||
|
// the OUT mark) unreachable, so the preview looped early.
|
||||||
var rangeIn=Math.max(0, Math.min(scene.duration, scene.inTime==null?0:scene.inTime));
|
var rangeIn=Math.max(0, Math.min(scene.duration, scene.inTime==null?0:scene.inTime));
|
||||||
var autoEnd=effectiveEnd(scene);
|
var rangeOut=Math.max(rangeIn, Math.min(scene.duration, scene.outTime==null?scene.duration:scene.outTime));
|
||||||
var rangeOut=Math.max(rangeIn, Math.min(scene.duration, scene.outTime==null?autoEnd:scene.outTime));
|
|
||||||
var sceneEnd=rangeOut;
|
var sceneEnd=rangeOut;
|
||||||
var cs=getComputedStyle(container);
|
var cs=getComputedStyle(container);
|
||||||
if(cs.position==='static')container.style.position='relative';
|
if(cs.position==='static')container.style.position='relative';
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const APP_VERSION = '0.8.0'
|
export const APP_VERSION = '0.8.1'
|
||||||
export const APP_NAME = 'RUWIO'
|
export const APP_NAME = 'RUWIO'
|
||||||
@@ -132,8 +132,9 @@ der clip-art drawer muss bevor er reingeschoben wird schon so tun als hätte er
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
die inline hilfe texte in den settings sind zu klein. das kann man nicht lesen
|
||||||
|
|
||||||
|
warum ist eigentlich still frame easing disabled? man muss doch zu einem still-frame hin easen können. verstehe den grund nicht
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -142,3 +143,5 @@ der clip-art drawer muss bevor er reingeschoben wird schon so tun als hätte er
|
|||||||
|
|
||||||
|
|
||||||
ich würde an dieser stelle mal ein paar regression-tests hinzufügen und dann committen
|
ich würde an dieser stelle mal ein paar regression-tests hinzufügen und dann committen
|
||||||
|
|
||||||
|
wenn man den leeren teil des content-views zieht (außerhalb der canvas constraints) muss canvas move auch ohne control key gehen
|
||||||
Reference in new issue
Block a user