From 3ef5fb5a2d75a22082558184645e6826ca20ae8c Mon Sep 17 00:00:00 2001 From: th Date: Mon, 20 Apr 2026 00:07:08 +0200 Subject: [PATCH] bak --- package.json | 2 +- server/render.html | 21 +++ src/App.vue | 27 ++- src/components/AboutDialog.vue | 177 ++++++++++++++++++++ src/components/AssetSidebar.vue | 32 +++- src/components/CanvasStage.vue | 260 +++++++++++++++++++++++++---- src/components/PropertiesPanel.vue | 152 +++++++++++++++++ src/components/Timeline.vue | 199 ++++++++++++++++------ src/composables/useKfSelection.ts | 23 +++ src/devtools/tests.ts | 254 ++++++++++++++++++++++++++++ src/i18n/messages.ts | 34 ++++ src/lib/exportPlayer.ts | 24 +++ src/lib/interpolate.ts | 13 ++ src/lib/render.ts | 74 +++++++- src/stores/scene.ts | 19 +++ src/styles/global.css | 8 + src/types/scene.ts | 8 + src/version.ts | 2 + todo | 9 +- 19 files changed, 1244 insertions(+), 94 deletions(-) create mode 100644 src/components/AboutDialog.vue create mode 100644 src/composables/useKfSelection.ts create mode 100644 src/version.ts diff --git a/package.json b/package.json index 6fbdecd..15c6884 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "animatr", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", diff --git a/server/render.html b/server/render.html index 024fa66..61013a3 100644 --- a/server/render.html +++ b/server/render.html @@ -95,17 +95,34 @@ ctx.translate(p.x,p.y); ctx.rotate(p.rotation*Math.PI/180); ctx.scale(p.scaleX,p.scaleY); + var sOp=p.shadowOpacity||0, sBl=p.shadowBlur||0, sDi=p.shadowDistance||0; + if(sOp>0 && (sBl>0 || sDi>0)){ + var sAn=(p.shadowAngle==null?90:p.shadowAngle)*Math.PI/180; + var sHex=(p.shadowColor||'#000000').replace('#',''); + if(sHex.length===3){sHex=sHex[0]+sHex[0]+sHex[1]+sHex[1]+sHex[2]+sHex[2];} + var sNum=parseInt(sHex.slice(0,6),16); if(isNaN(sNum))sNum=0; + ctx.shadowColor='rgba('+((sNum>>16)&255)+','+((sNum>>8)&255)+','+(sNum&255)+','+sOp+')'; + ctx.shadowBlur=sBl; + ctx.shadowOffsetX=Math.cos(sAn)*sDi; + ctx.shadowOffsetY=Math.sin(sAn)*sDi; + } var fill = p.fill || asset.fill; if(asset.kind==='rect'){ var rOffX=layer.offsetX!=null?layer.offsetX:asset.width/2; var rOffY=layer.offsetY!=null?layer.offsetY:asset.height/2; + var rBlur=(p.blur!=null?p.blur:(asset.blur||0)); ctx.fillStyle=fill; + if(rBlur>0) ctx.filter='blur('+rBlur+'px)'; ctx.fillRect(-rOffX,-rOffY,asset.width,asset.height); + ctx.filter='none'; } else if(asset.kind==='circle'){ var cOffX=layer.offsetX!=null?layer.offsetX:0; var cOffY=layer.offsetY!=null?layer.offsetY:0; + var cBlur=(p.blur!=null?p.blur:(asset.blur||0)); ctx.fillStyle=fill; + if(cBlur>0) ctx.filter='blur('+cBlur+'px)'; ctx.beginPath(); ctx.arc(-cOffX,-cOffY,asset.radius,0,Math.PI*2); ctx.fill(); + ctx.filter='none'; } else if(asset.kind==='text'){ var tOffX=layer.offsetX!=null?layer.offsetX:0; var tOffY=layer.offsetY!=null?layer.offsetY:0; @@ -121,6 +138,10 @@ ctx.drawImage(img,-iOffX,-iOffY,asset.width,asset.height); } } + ctx.shadowColor='transparent'; + ctx.shadowBlur=0; + ctx.shadowOffsetX=0; + ctx.shadowOffsetY=0; ctx.restore(); } function drawFrame(time){ diff --git a/src/App.vue b/src/App.vue index d2c17f2..f839ec2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -15,6 +15,7 @@ import { PhSparkle, PhStop, } from '@phosphor-icons/vue' +import AboutDialog from './components/AboutDialog.vue' import AssetSidebar from './components/AssetSidebar.vue' import CanvasStage from './components/CanvasStage.vue' import DocumentSettings from './components/DocumentSettings.vue' @@ -48,6 +49,7 @@ const exportOpen = ref(false) const testsOpen = ref(false) const welcomeOpen = ref(false) const fullscreenOpen = ref(false) +const aboutOpen = ref(false) const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight' const DEFAULT_TIMELINE_HEIGHT = 220 const MIN_TIMELINE_HEIGHT = 120 @@ -237,6 +239,12 @@ function isEditable(el: EventTarget | null): boolean { return false } +function onDocumentContextMenu(e: MouseEvent) { + // Allow native context menu only on editable inputs (for paste, etc.). + if (isEditable(e.target)) return + e.preventDefault() +} + function onKeyDown(e: KeyboardEvent) { if (e.ctrlKey && e.shiftKey && e.altKey && (e.code === 'KeyT' || e.key.toLowerCase() === 't')) { e.preventDefault() @@ -270,11 +278,13 @@ function onKeyDown(e: KeyboardEvent) { onMounted(() => { document.addEventListener('keydown', onKeyDown) + document.addEventListener('contextmenu', onDocumentContextMenu) window.addEventListener('resize', onWindowResize) bootWelcome() }) onBeforeUnmount(() => { document.removeEventListener('keydown', onKeyDown) + document.removeEventListener('contextmenu', onDocumentContextMenu) window.removeEventListener('resize', onWindowResize) }) @@ -283,10 +293,10 @@ onBeforeUnmount(() => {
- +
{ + { display: inline-flex; align-items: center; gap: 6px; + padding: 4px 8px; + margin-left: -8px; + background: transparent; + border: 1px solid transparent; + color: var(--text); + border-radius: var(--radius-sm); + cursor: pointer; +} + +.brand:hover { + background: var(--panel-alt); + border-color: var(--border); } .brand-icon { diff --git a/src/components/AboutDialog.vue b/src/components/AboutDialog.vue new file mode 100644 index 0000000..ee689d0 --- /dev/null +++ b/src/components/AboutDialog.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/src/components/AssetSidebar.vue b/src/components/AssetSidebar.vue index fcd7751..3de2c1a 100644 --- a/src/components/AssetSidebar.vue +++ b/src/components/AssetSidebar.vue @@ -175,11 +175,35 @@ const imageAssets = computed(() => store.scene.assets.filter((a): a is Extract => a.kind === 'image'), ) -function onAssetDragStart(e: DragEvent, assetId: string) { +function onAssetDragStart(e: DragEvent, asset: Extract) { if (!e.dataTransfer) return e.dataTransfer.effectAllowed = 'copy' - e.dataTransfer.setData('application/x-animatr-asset', assetId) - e.dataTransfer.setData('text/plain', assetId) + e.dataTransfer.setData('application/x-animatr-asset', asset.id) + e.dataTransfer.setData('text/plain', asset.id) + + // Use the actual graphic as drag ghost instead of the library entry. + const target = e.currentTarget as HTMLElement + const rect = target.getBoundingClientRect() + const thumbImg = target.querySelector('img') as HTMLImageElement | null + if (thumbImg && thumbImg.complete && thumbImg.naturalWidth > 0) { + const r = thumbImg.getBoundingClientRect() + const ox = Math.max(0, e.clientX - r.left) + const oy = Math.max(0, e.clientY - r.top) + e.dataTransfer.setDragImage(thumbImg, ox, oy) + return + } + // Fallback — create an offscreen image of the asset for use as drag ghost. + const ghost = document.createElement('img') + ghost.src = asset.src + ghost.style.cssText = + 'position:fixed;top:-10000px;left:-10000px;width:72px;height:auto;pointer-events:none;' + document.body.appendChild(ghost) + const gx = Math.max(0, e.clientX - rect.left) + const gy = Math.max(0, e.clientY - rect.top) + e.dataTransfer.setDragImage(ghost, gx, gy) + setTimeout(() => { + ghost.remove() + }, 0) } @@ -234,7 +258,7 @@ function onAssetDragStart(e: DragEvent, assetId: string) { v-for="asset in imageAssets" :key="asset.id" draggable="true" - @dragstart="onAssetDragStart($event, asset.id)" + @dragstart="onAssetDragStart($event, asset)" > @@ -755,6 +956,7 @@ onBeforeUnmount(() => { v-if="rotationHandleConfig" :config="rotationHandleConfig" @dragmove="onRotationHandleDragMove" + @dragend="onRotationHandleDragEnd" > diff --git a/src/components/PropertiesPanel.vue b/src/components/PropertiesPanel.vue index 68d0338..863b604 100644 --- a/src/components/PropertiesPanel.vue +++ b/src/components/PropertiesPanel.vue @@ -34,6 +34,76 @@ const textAsset = computed(() => { return a && a.kind === 'text' ? a : null }) +const blurAsset = computed(() => { + const a = selectedAsset.value + return a && (a.kind === 'rect' || a.kind === 'circle') ? a : null +}) + +const blurValue = computed({ + get: () => { + const a = blurAsset.value + if (!a) return 0 + return sampled.value?.blur ?? a.blur ?? 0 + }, + set: (v: number) => { + if (!selected.value) return + store.upsertKeyframe(selected.value.id, store.playhead, { blur: v }) + }, +}) + +type ShadowNumKey = 'shadowBlur' | 'shadowOpacity' | 'shadowAngle' | 'shadowDistance' + +interface ShadowAll { + shadowBlur: number + shadowOpacity: number + shadowAngle: number + shadowDistance: number + shadowColor: string +} + +const SHADOW_DEFAULTS: ShadowAll = { + shadowBlur: 8, + shadowOpacity: 0, + shadowAngle: 90, + shadowDistance: 6, + shadowColor: '#000000', +} + +function currentShadow(): ShadowAll { + const samp = sampled.value + return { + shadowBlur: samp?.shadowBlur ?? SHADOW_DEFAULTS.shadowBlur, + shadowOpacity: samp?.shadowOpacity ?? SHADOW_DEFAULTS.shadowOpacity, + shadowAngle: samp?.shadowAngle ?? SHADOW_DEFAULTS.shadowAngle, + shadowDistance: samp?.shadowDistance ?? SHADOW_DEFAULTS.shadowDistance, + shadowColor: samp?.shadowColor ?? SHADOW_DEFAULTS.shadowColor, + } +} + +function writeShadow(patch: Partial) { + if (!selected.value) return + const next = { ...currentShadow(), ...patch } + store.upsertKeyframe(selected.value.id, store.playhead, next) +} + +function shadowBinding(key: ShadowNumKey, decimals = 0) { + const factor = 10 ** decimals + return computed({ + get: () => Math.round(currentShadow()[key] * factor) / factor, + set: (v: number) => writeShadow({ [key]: v } as Partial), + }) +} + +const shadowBlur = shadowBinding('shadowBlur') +const shadowOpacity = shadowBinding('shadowOpacity', 2) +const shadowAngle = shadowBinding('shadowAngle') +const shadowDistance = shadowBinding('shadowDistance') + +const shadowColorValue = computed({ + get: () => currentShadow().shadowColor, + set: (v: string) => writeShadow({ shadowColor: v }), +}) + const canFill = computed(() => { const a = selectedAsset.value return !!a && (a.kind === 'rect' || a.kind === 'circle' || a.kind === 'text') @@ -108,6 +178,71 @@ function removeKeyframeHere() { + +
+

{{ t('properties.shadow') }}

+ + + + + +