0.8.2: pan outside canvas + finish RUWIO rename of exports
- Canvas stage: plain left-click in the empty content area (outside
the canvas rectangle) now starts a pan. Ctrl+left-click still pans
anywhere, middle-click unchanged. Only pure left-click qualifies
(any modifier leaves the old routing intact).
- Export rebrand Animatr → RUWIO for everything user-visible:
- Default filenames: projekt.animatr.json → projekt.ruwio.json;
animatr-runtime.js → ruwio-runtime.js; App's export fallback
base animatr → ruwio.
- Generated comment headers in runtime/scene/bundled scripts.
- Console warn prefix in the runtime and the scene store.
- Example HTML title, heading, and embed-usage snippet.
- parseProject error message.
- ExportDialog copy.
- RUNTIME_VERSION 14 → 15 so cached runtimes reload.
- On-disk magic stays `animatr-project` and window.AnimatrPlayer /
__animatrPending / localStorage & IndexedDB keys are untouched so
old project files and previously-exported HTML keep working.
Regression tests: legacy magic still parses; parseProject error
mentions RUWIO and not Animatr; runtime/scene/bundled scripts and
example HTML all carry the new brand; console prefix is [ruwio].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
82ee5c98d0
commit
a2c8e5998a
10 files changed
+107
-19
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "animatr",
|
"name": "animatr",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+2
-2
@@ -218,8 +218,8 @@ async function renameProject() {
|
|||||||
|
|
||||||
function exportProjectFile() {
|
function exportProjectFile() {
|
||||||
const scene = JSON.parse(JSON.stringify(store.scene))
|
const scene = JSON.parse(JSON.stringify(store.scene))
|
||||||
const base = currentProjectName.value ?? 'animatr'
|
const base = currentProjectName.value ?? 'ruwio'
|
||||||
const safe = base.replace(/[^a-z0-9äöüß_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'animatr'
|
const safe = base.replace(/[^a-z0-9äöüß_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'ruwio'
|
||||||
downloadProject(scene, `${safe}.json`)
|
downloadProject(scene, `${safe}.json`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2597,9 +2597,30 @@ let panStartViewX = 0
|
|||||||
let panStartViewY = 0
|
let panStartViewY = 0
|
||||||
let panPointerId = -1
|
let panPointerId = -1
|
||||||
|
|
||||||
|
function isInsideCanvasBounds(e: PointerEvent | MouseEvent): boolean {
|
||||||
|
const rect = container.value?.getBoundingClientRect()
|
||||||
|
if (!rect) return true
|
||||||
|
const localX = e.clientX - rect.left
|
||||||
|
const localY = e.clientY - rect.top
|
||||||
|
const cw = effectiveCanvas.value.width * zoom.value
|
||||||
|
const ch = effectiveCanvas.value.height * zoom.value
|
||||||
|
return (
|
||||||
|
localX >= viewX.value &&
|
||||||
|
localX <= viewX.value + cw &&
|
||||||
|
localY >= viewY.value &&
|
||||||
|
localY <= viewY.value + ch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function isPanTrigger(e: PointerEvent | MouseEvent) {
|
function isPanTrigger(e: PointerEvent | MouseEvent) {
|
||||||
if (e.button === 1) return true
|
if (e.button === 1) return true
|
||||||
if (e.button === 0 && e.ctrlKey) return true
|
if (e.button === 0 && e.ctrlKey) return true
|
||||||
|
// Plain left-click on the empty content view (outside the canvas
|
||||||
|
// rectangle) pans the viewport too — saves a ctrl-modifier for the
|
||||||
|
// common case of re-centering after zoom.
|
||||||
|
if (e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey && !isInsideCanvasBounds(e)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ function onUpdate(v: boolean) {
|
|||||||
<div class="opt-icon"><PhEngine :size="20" /></div>
|
<div class="opt-icon"><PhEngine :size="20" /></div>
|
||||||
<div class="opt-text">
|
<div class="opt-text">
|
||||||
<div class="opt-title">
|
<div class="opt-title">
|
||||||
animatr-runtime.js
|
ruwio-runtime.js
|
||||||
<span class="badge">{{ t('export.runtimeBadge') }}</span>
|
<span class="badge">{{ t('export.runtimeBadge') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="opt-desc">{{ t('export.runtimeDesc') }}</div>
|
<div class="opt-desc">{{ t('export.runtimeDesc') }}</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { isLayerActiveAt, sampleLayer, lerpColor } from '@/lib/interpolate'
|
|||||||
import { snapToCandidates } from '@/lib/timelineSnap'
|
import { snapToCandidates } from '@/lib/timelineSnap'
|
||||||
import {
|
import {
|
||||||
generateBundledScript,
|
generateBundledScript,
|
||||||
|
generateExampleHtml,
|
||||||
generateRuntimeScript,
|
generateRuntimeScript,
|
||||||
generateSceneScript,
|
generateSceneScript,
|
||||||
} from '@/lib/exportPlayer'
|
} from '@/lib/exportPlayer'
|
||||||
@@ -699,6 +700,40 @@ export function registerAllTests(): void {
|
|||||||
// And the current fallback must be scene.duration.
|
// And the current fallback must be scene.duration.
|
||||||
expect(r).toContain('scene.outTime==null?scene.duration:scene.outTime')
|
expect(r).toContain('scene.outTime==null?scene.duration:scene.outTime')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('export comments and console prefix use RUWIO, not Animatr', () => {
|
||||||
|
// Rebrand: generated comment headers, the runtime's console warn
|
||||||
|
// prefix, and the example-HTML chrome all read RUWIO now. If any
|
||||||
|
// of these come back as "Animatr", we regressed.
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const runtime = generateRuntimeScript()
|
||||||
|
expect(runtime).toContain('RUWIO runtime')
|
||||||
|
expect(runtime.indexOf('/* Animatr ')).toBe(-1)
|
||||||
|
// Console prefix used by the runtime's action evaluator.
|
||||||
|
expect(runtime).toContain("'[ruwio]'")
|
||||||
|
expect(runtime.indexOf("'[animatr]'")).toBe(-1)
|
||||||
|
|
||||||
|
const sceneScript = generateSceneScript(s.scene)
|
||||||
|
expect(sceneScript).toContain('RUWIO scene')
|
||||||
|
expect(sceneScript).toContain('ruwio-runtime.js')
|
||||||
|
|
||||||
|
const bundled = generateBundledScript(s.scene)
|
||||||
|
expect(bundled).toContain('RUWIO bundled export')
|
||||||
|
expect(bundled.indexOf('/* Animatr ')).toBe(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generated example HTML is RUWIO-branded', () => {
|
||||||
|
// The example HTML is what users see when they drop the file
|
||||||
|
// into a browser. Title, heading, and the embed-usage snippet
|
||||||
|
// all need to carry the new brand.
|
||||||
|
const s = makeFreshStore()
|
||||||
|
const html = generateExampleHtml(s.scene)
|
||||||
|
expect(html).toContain('<title>RUWIO — Example</title>')
|
||||||
|
expect(html).toContain('<h1>RUWIO Export</h1>')
|
||||||
|
// The usage snippet now references ruwio-runtime.js.
|
||||||
|
expect(html).toContain('ruwio-runtime.js')
|
||||||
|
expect(html.indexOf('animatr-runtime.js')).toBe(-1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('playback — transport', () => {
|
suite('playback — transport', () => {
|
||||||
@@ -818,6 +853,34 @@ export function registerAllTests(): void {
|
|||||||
}
|
}
|
||||||
expect(caught instanceof ProjectFileError).toBe(true)
|
expect(caught instanceof ProjectFileError).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('legacy `animatr-project` magic still parses (backward compat)', () => {
|
||||||
|
// When we rebranded to RUWIO we kept the on-disk magic as
|
||||||
|
// `animatr-project` so older exports continue to load. Pin that
|
||||||
|
// so a future cleanup doesn't accidentally break existing user
|
||||||
|
// files on disk.
|
||||||
|
const s = makeFreshStore()
|
||||||
|
s.addAsset({ kind: 'rect', name: 'Legacy', width: 10, height: 10, fill: '#123' })
|
||||||
|
const legacyPayload = JSON.parse(serializeProject(s.scene))
|
||||||
|
expect(legacyPayload.magic).toBe('animatr-project')
|
||||||
|
// And a fully-formed legacy file parses without error.
|
||||||
|
const scene = parseProject(JSON.stringify(legacyPayload))
|
||||||
|
expect(scene.assets.some((a) => a.name === 'Legacy')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parseProject error mentions RUWIO, not Animatr', () => {
|
||||||
|
// The user-visible error when a non-project JSON is dropped in
|
||||||
|
// reads "Not a RUWIO project file …" now. If you rebrand again
|
||||||
|
// remember to update this test alongside the string.
|
||||||
|
let msg = ''
|
||||||
|
try {
|
||||||
|
parseProject(JSON.stringify({ not: 'a project' }))
|
||||||
|
} catch (e) {
|
||||||
|
msg = (e as Error).message
|
||||||
|
}
|
||||||
|
expect(msg).toContain('RUWIO')
|
||||||
|
expect(msg.toLowerCase().includes('animatr')).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
suite('history — undo/redo', () => {
|
suite('history — undo/redo', () => {
|
||||||
|
|||||||
+10
-10
@@ -1,6 +1,6 @@
|
|||||||
import type { Scene } from '@/types/scene'
|
import type { Scene } from '@/types/scene'
|
||||||
|
|
||||||
const RUNTIME_VERSION = '14'
|
const RUNTIME_VERSION = '15'
|
||||||
|
|
||||||
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;
|
||||||
@@ -194,7 +194,7 @@ const ENGINE_SOURCE = `(function(){
|
|||||||
forcePlay: false,
|
forcePlay: false,
|
||||||
buttonStates: {},
|
buttonStates: {},
|
||||||
buttonDisabled: {},
|
buttonDisabled: {},
|
||||||
warn: function(m){ if(window.console)window.console.warn('[animatr]',m); }
|
warn: function(m){ if(window.console)window.console.warn('[ruwio]',m); }
|
||||||
};
|
};
|
||||||
(scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; });
|
(scene.markers||[]).forEach(function(mk){ runtime.markers[mk.name]=mk.time; });
|
||||||
|
|
||||||
@@ -897,7 +897,7 @@ export interface ExportOptions {
|
|||||||
|
|
||||||
export function generateRuntimeScript(): string {
|
export function generateRuntimeScript(): string {
|
||||||
return (
|
return (
|
||||||
'/* Animatr runtime v' + RUNTIME_VERSION + ' — generated ' + new Date().toISOString() + ' */\n' +
|
'/* RUWIO runtime v' + RUNTIME_VERSION + ' — generated ' + new Date().toISOString() + ' */\n' +
|
||||||
ENGINE_SOURCE
|
ENGINE_SOURCE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -906,8 +906,8 @@ export function generateSceneScript(scene: Scene, options: ExportOptions = {}):
|
|||||||
const sceneJson = jsonForExport(scene)
|
const sceneJson = jsonForExport(scene)
|
||||||
const optsJson = JSON.stringify({ loop: options.loop !== false })
|
const optsJson = JSON.stringify({ loop: options.loop !== false })
|
||||||
return (
|
return (
|
||||||
'/* Animatr scene — generated ' + new Date().toISOString() + '\n' +
|
'/* RUWIO scene — generated ' + new Date().toISOString() + '\n' +
|
||||||
' * Requires animatr-runtime.js (v' + RUNTIME_VERSION + ') to be loaded once per page.\n' +
|
' * Requires ruwio-runtime.js (v' + RUNTIME_VERSION + ') to be loaded once per page.\n' +
|
||||||
' */\n' +
|
' */\n' +
|
||||||
SCENE_BOOT_SOURCE.replace('__SCENE__', sceneJson).replace('__OPTIONS__', optsJson)
|
SCENE_BOOT_SOURCE.replace('__SCENE__', sceneJson).replace('__OPTIONS__', optsJson)
|
||||||
)
|
)
|
||||||
@@ -915,7 +915,7 @@ export function generateSceneScript(scene: Scene, options: ExportOptions = {}):
|
|||||||
|
|
||||||
export function generateBundledScript(scene: Scene, options: ExportOptions = {}): string {
|
export function generateBundledScript(scene: Scene, options: ExportOptions = {}): string {
|
||||||
return (
|
return (
|
||||||
'/* Animatr bundled export — generated ' + new Date().toISOString() + ' */\n' +
|
'/* RUWIO bundled export — generated ' + new Date().toISOString() + ' */\n' +
|
||||||
ENGINE_SOURCE +
|
ENGINE_SOURCE +
|
||||||
'\n' +
|
'\n' +
|
||||||
generateSceneScriptBody(scene, options)
|
generateSceneScriptBody(scene, options)
|
||||||
@@ -928,7 +928,7 @@ function generateSceneScriptBody(scene: Scene, options: ExportOptions): string {
|
|||||||
return SCENE_BOOT_SOURCE.replace('__SCENE__', sceneJson).replace('__OPTIONS__', optsJson)
|
return SCENE_BOOT_SOURCE.replace('__SCENE__', sceneJson).replace('__OPTIONS__', optsJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadRuntime(filename = 'animatr-runtime.js') {
|
export function downloadRuntime(filename = 'ruwio-runtime.js') {
|
||||||
triggerDownload(generateRuntimeScript(), filename, 'application/javascript')
|
triggerDownload(generateRuntimeScript(), filename, 'application/javascript')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,7 +949,7 @@ export function generateExampleHtml(scene: Scene, options: ExportOptions = {}):
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>Animatr — Example</title>
|
<title>RUWIO — Example</title>
|
||||||
<style>
|
<style>
|
||||||
:root { color-scheme: light dark; }
|
:root { color-scheme: light dark; }
|
||||||
body {
|
body {
|
||||||
@@ -1003,7 +1003,7 @@ export function generateExampleHtml(scene: Scene, options: ExportOptions = {}):
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Animatr Export</h1>
|
<h1>RUWIO Export</h1>
|
||||||
<p>Load the runtime once, then embed as many scenes on the page as you like.</p>
|
<p>Load the runtime once, then embed as many scenes on the page as you like.</p>
|
||||||
|
|
||||||
<!-- Runtime: once per page -->
|
<!-- Runtime: once per page -->
|
||||||
@@ -1021,7 +1021,7 @@ ${sceneBoot}
|
|||||||
<div class="usage">
|
<div class="usage">
|
||||||
<h2>Embed on your own page</h2>
|
<h2>Embed on your own page</h2>
|
||||||
<pre><!-- once per page -->
|
<pre><!-- once per page -->
|
||||||
<script src="animatr-runtime.js"></script>
|
<script src="ruwio-runtime.js"></script>
|
||||||
|
|
||||||
<!-- per animation: one container + scene script -->
|
<!-- per animation: one container + scene script -->
|
||||||
<div style="width: 800px; height: 450px;">
|
<div style="width: 800px; height: 450px;">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Scene } from '@/types/scene'
|
import type { Scene } from '@/types/scene'
|
||||||
|
|
||||||
const FILE_VERSION = 1
|
const FILE_VERSION = 1
|
||||||
|
// The on-disk magic stays `animatr-project` so older exports keep loading.
|
||||||
|
// Only user-visible filenames & strings have rebranded to RUWIO.
|
||||||
const MAGIC = 'animatr-project'
|
const MAGIC = 'animatr-project'
|
||||||
|
|
||||||
export interface ProjectFile {
|
export interface ProjectFile {
|
||||||
@@ -39,7 +41,7 @@ export function parseProject(text: string): Scene {
|
|||||||
}
|
}
|
||||||
const obj = data as Record<string, unknown>
|
const obj = data as Record<string, unknown>
|
||||||
if (obj.magic !== MAGIC) {
|
if (obj.magic !== MAGIC) {
|
||||||
throw new ProjectFileError('Not an Animatr project file (magic missing).')
|
throw new ProjectFileError('Not a RUWIO project file (magic missing).')
|
||||||
}
|
}
|
||||||
const scene = obj.scene
|
const scene = obj.scene
|
||||||
if (!scene || typeof scene !== 'object') {
|
if (!scene || typeof scene !== 'object') {
|
||||||
@@ -58,7 +60,7 @@ export function parseProject(text: string): Scene {
|
|||||||
return scene as Scene
|
return scene as Scene
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadProject(scene: Scene, filename = 'projekt.animatr.json') {
|
export function downloadProject(scene: Scene, filename = 'projekt.ruwio.json') {
|
||||||
const source = serializeProject(scene)
|
const source = serializeProject(scene)
|
||||||
const blob = new Blob([source], { type: 'application/json' })
|
const blob = new Blob([source], { type: 'application/json' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
|
|||||||
+1
-1
@@ -1760,7 +1760,7 @@ export const useSceneStore = defineStore('scene', {
|
|||||||
},
|
},
|
||||||
warn(msg: string) {
|
warn(msg: string) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[animatr]', msg)
|
console.warn('[ruwio]', msg)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const APP_VERSION = '0.8.1'
|
export const APP_VERSION = '0.8.2'
|
||||||
export const APP_NAME = 'RUWIO'
|
export const APP_NAME = 'RUWIO'
|
||||||
@@ -144,4 +144,6 @@ warum ist eigentlich still frame easing disabled? man muss doch zu einem still-f
|
|||||||
|
|
||||||
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
|
wenn man den leeren teil des content-views zieht (außerhalb der canvas constraints) muss canvas move auch ohne control key gehen
|
||||||
|
|
||||||
|
export dateien heißen noch animatr. wir haben in RUWIO umbenannt
|
||||||
Reference in new issue
Block a user