@designbasekorea/ui-wc 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +4 -0
- package/dist/esm/components/db-design-canvas.js +1 -1
- package/dist/esm/components/db-video-timeline.d.ts +12 -1
- package/dist/esm/components/db-video-timeline.js +1 -1
- package/dist/esm/components/media-editor-model.d.ts +5 -0
- package/dist/esm/core/i18n-copy.js +1 -1
- package/dist/esm/core/i18n-en.js +1 -1
- package/dist/esm/core/i18n.d.ts +4 -0
- package/dist/esm/core/i18n.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.esm.js +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.esm.js +1 -1
- package/docs/COMPONENT-USAGE.md +206 -0
- package/docs/EFFECT-KEYFRAMES.md +7 -0
- package/docs/MEDIA-EDITORS.md +20 -0
- package/docs/TOOL-UI.md +200 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.17.0 - 2026-11-08
|
|
6
|
+
|
|
7
|
+
- Timeline layers now expand from their track-name chevrons; source transform channels are separate from indented applied effects.
|
|
8
|
+
- Applied effects have independent move/trim ranges with draft/commit, cancellation and undo support.
|
|
9
|
+
- Added layerGap (0–32px) to separate multiple parent layers while keeping their properties/effects grouped.
|
|
10
|
+
|
|
11
|
+
## 0.16.0 - 2026-11-07
|
|
12
|
+
|
|
13
|
+
- Timeline: resizable 120–480px label panel with persisted resize events and one-line property ellipsis.
|
|
14
|
+
- Single-effect timelines use one disclosure; clips can move beyond the composition boundary with partial fading and stable controlled state.
|
|
15
|
+
|
|
16
|
+
- Removed unnecessary property settings icons; X/Y/Scale/Opacity labels remain visible and left-aligned in compact rows.
|
|
17
|
+
|
|
5
18
|
## 0.15.0 - 2026-11-07
|
|
6
19
|
|
|
7
20
|
- LayerList: handle/whole-row pointer reorder, visibility/lock/delete and custom actions; keyboard navigation and edge autoscroll; React, Storybook, localized docs and responsive/theme coverage.
|
package/README.md
CHANGED
|
@@ -533,6 +533,10 @@ AI 대화·작업 과정·출처·도구 결과는 [AI Elements](./docs/AI-ELEME
|
|
|
533
533
|
|
|
534
534
|
## 컴포넌트 패키지만 배포
|
|
535
535
|
|
|
536
|
+
화면을 처음 구성할 때는 [페이지 유형별 컴포넌트 선택과 사용법](docs/COMPONENT-USAGE.md)을 먼저 참고하세요. 소개 페이지·SaaS 앱의 셸, Sidebar의 app/navigation/category 구분, Dropdown과 Select의 차이, FilterBar 조합과 Modal 저장 흐름을 다룹니다.
|
|
537
|
+
|
|
538
|
+
편집기·유틸리티는 [ToolLayout·ToolPanel 상세 사용법](docs/TOOL-UI.md)을 참고하세요. 영역 배치와 패널 구성, 탭·접기·닫기·복원, 리사이즈와 도구 상태 연결을 다룹니다.
|
|
539
|
+
|
|
536
540
|
저장소 루트에서 `npm run ui-wc:build`로 공유 계약과 ui-wc만 빌드하고, `npm run publish:ui-wc`로 `@designbasekorea/ui-wc`만 npm에 배포합니다. 이 명령은 blocks·템플릿·갤러리를 배포하지 않습니다. 갤러리 전체 빌드는 `npm run example:gallery:build`가 blocks 빌드를 별도로 실행합니다.
|
|
537
541
|
|
|
538
542
|
npm 배포물에는 ui-wc의 `dist`, README·변경 이력과 결제 로고 라이선스 고지만 포함됩니다. 신규 컴포넌트는 전체 진입점·React 래퍼·개별 import에서 사용할 수 있습니다. 결제·초대·AI 요청·저장 같은 실제 서버 동작은 소비 앱에서 연결합니다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dbText as t,dbFormat as e}from"../core/i18n.js";import"./db-button.js";import"./db-input.js";import"./db-card.js";import"./db-badge.js";const clamp=(t,e,i)=>Math.min(i,Math.max(e,t)),num=(t,e)=>"number"==typeof t&&Number.isFinite(t)?t:e,clone=t=>t.map(t=>({...t})),same=(t,e)=>t.length===e.length&&t.every((t,i)=>Object.keys({...t,...e[i]}).every(s=>t[s]===e[i][s]));class DbDesignCanvas extends HTMLElement{constructor(){super(...arguments),this.data=[],this.selected=null,this.view={x:0,y:0,zoom:1},this.objectElements=new Map,this.slots=new Map,this.pointers=new Map,this.space=!1,this.hand=!1,this.creationTool="frame",this.shapeTool="rectangle",this.componentTool="button",this.past=[],this.future=[],this.reading=new Set,this.generation=0,this.beginTextEdit=e=>{if(this.hand||this.space)return;const i=e.target.closest("[data-node-id]")||document.elementFromPoint(e.clientX,e.clientY)?.closest("[data-node-id]"),s=this.data.find(t=>t.id===i?.dataset.nodeId);if(!s||"text"!==s.type||s.locked||this.readOnly)return;const o=i.querySelector(".designbase-wc-design-canvas__text");if(o.isContentEditable)return;e.preventDefault(),this.editing={id:s.id,before:this.nodes},o.contentEditable="plaintext-only",o.setAttribute("role","textbox"),o.setAttribute("aria-label",t("텍스트 편집")),o.tabIndex=0,o.focus();const finish=(t=!1)=>{if(this.editing?.id!==s.id)return;const e=this.editing.before;this.editing=void 0,o.removeEventListener("input",input),o.removeEventListener("keydown",key),o.removeEventListener("blur",blur),o.contentEditable="false",o.removeAttribute("role"),o.removeAttribute("aria-label"),o.tabIndex=-1,t?this.data=e:same(e,this.data)||this.remember(e),this.emit(t?"text-cancel":"text")},input=()=>{const t=(o.innerText||"").slice(0,1e4);this.data=this.data.map(e=>e.id===s.id?{...e,text:t}:e),this.emit("text-input")},key=t=>{("Escape"===t.key||"Enter"===t.key&&(t.ctrlKey||t.metaKey))&&(t.preventDefault(),t.stopPropagation(),finish("Escape"===t.key),o.blur(),this.surface?.focus({preventScroll:!0}))},blur=()=>finish();o.addEventListener("input",input),o.addEventListener("keydown",key),o.addEventListener("blur",blur)},this.toolAction=t=>{t.stopPropagation();const{id:e,optionId:i}=t.detail;if("select"===e||"pan"===e)return this.hand="pan"===e,void this.drawViewport();if("image"===e)return void(this.readOnly||this.input?.click());if("undo"===e)return void this.undo();if("redo"===e)return void this.redo();if("zoom-in"===e)return void this.zoomTo(1.2*this.view.zoom);if("zoom-out"===e)return void this.zoomTo(this.view.zoom/1.2);if("fit"===e||"zoom"===e&&"fit"===i)return void this.fitToContent();if("zoom"===e){const t=Number(i)/100;return void(t>0&&this.zoomTo(t))}if(this.readOnly)return;if(["frame","text","shape","component"].includes(e))return void this.insertNode(e,i);const s=this.data.find(t=>t.id===this.selected);if(s&&(!s.locked||"lock"===e)&&("duplicate"!==e||!(s.slot||this.data.length>=100))){if(this.remember(this.data),"duplicate"===e){const t={...s,id:crypto.randomUUID(),name:`${s.name} 복사`,x:s.x+24,y:s.y+24};this.data=[...this.data,t],this.select(t.id)}else if("delete"===e)this.data=this.data.filter(t=>t.id!==s.id),this.select(null);else if("arrange"===e){const t=this.data.filter(t=>t.id!==s.id);this.data="front"===i?[...t,s]:[s,...t]}else{const t={...s};if("lock"===e&&(t.locked=!s.locked),"rotate"===e&&(t.rotation="reset"===i?0:(s.rotation||0)+("left"===i?-90:90)),"flip-x"===e&&(t.flipX=!s.flipX),"flip-y"===e&&(t.flipY=!s.flipY),("align"===e||e.startsWith("align-"))&&this.surface){const o="align"===e?i:e.slice(6),n=-this.view.x/this.view.zoom,a=-this.view.y/this.view.zoom,r=this.surface.clientWidth/this.view.zoom/2,h=this.surface.clientHeight/this.view.zoom/2,d=(s.rotation||0)*Math.PI/180,l=Math.abs(s.width*Math.cos(d))+Math.abs(s.height*Math.sin(d)),c=Math.abs(s.width*Math.sin(d))+Math.abs(s.height*Math.cos(d));"center"===o&&(t.x=n-s.width/2),"middle"===o&&(t.y=a-s.height/2),"left"===o&&(t.x=n-r+(l-s.width)/2+24),"right"===o&&(t.x=n+r-(l+s.width)/2-24),"top"===o&&(t.y=a-h+(c-s.height)/2+24),"bottom"===o&&(t.y=a+h-(c+s.height)/2-24)}this.data=this.data.map(e=>e.id===s.id?t:e)}this.emit(e)}},this.onPointerDown=t=>{if(t.target.closest("db-tool-button,db-design-toolbar,db-context-bar"))return;if(0!==t.button&&1!==t.button)return;if(this.pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),"touch"===t.pointerType&&2===this.pointers.size){this.cancelGesture();const[e,i]=[...this.pointers.values()],s={clientX:(e.x+i.x)/2,clientY:(e.y+i.y)/2};return this.pinch={distance:Math.hypot(e.x-i.x,e.y-i.y),zoom:this.view.zoom,world:this.point(s)},void this.surface.setPointerCapture(t.pointerId)}if(this.editable(t))return;const e=t.target,i=e.closest("[data-handle]")?.dataset.handle,s=e.closest("[data-node-id]")?.dataset.nodeId;s&&this.select(s);const o=this.data.find(t=>t.id===this.selected),n=this.space||this.hand||1===t.button;if(!n&&!i&&!s)return this.surface.focus({preventScroll:!0}),this.surface.setPointerCapture(t.pointerId),void this.select(null);if((n||o&&!o.locked&&!this.readOnly)&&(n||!e.closest("db-button,button,a,input,textarea,select")||i)){if(t.preventDefault(),this.surface.focus({preventScroll:!0}),this.gesture={kind:n?"pan":"rotate"===i?"rotate":i?"resize":"move",pointer:t.pointerId,start:{x:t.clientX,y:t.clientY},viewport:this.viewport,node:o?{...o}:void 0,handle:i,before:this.nodes},"rotate"===this.gesture.kind&&o){const e=this.point(t);this.gesture.angle=Math.atan2(e.y-o.y-o.height/2,e.x-o.x-o.width/2)}this.surface.setPointerCapture(t.pointerId),this.drawViewport()}},this.onPointerMove=t=>{if(this.pointers.has(t.pointerId)&&this.pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),this.pinch&&this.pointers.size>=2){const[t,e]=[...this.pointers.values()],i=this.surface.getBoundingClientRect();return this.view.zoom=clamp(this.pinch.zoom*Math.hypot(t.x-e.x,t.y-e.y)/Math.max(1,this.pinch.distance),this.minZoom,this.maxZoom),this.view.x=(t.x+e.x)/2-i.left-this.surface.clientWidth/2-this.pinch.world.x*this.view.zoom,this.view.y=(t.y+e.y)/2-i.top-this.surface.clientHeight/2-this.pinch.world.y*this.view.zoom,void this.emitViewport()}const e=this.gesture;if(!e||e.pointer!==t.pointerId)return;if("pan"===e.kind)return this.view.x=e.viewport.x+t.clientX-e.start.x,this.view.y=e.viewport.y+t.clientY-e.start.y,void this.emitViewport();const i=e.node;let s={...i};const o=(t.clientX-e.start.x)/e.viewport.zoom,n=(t.clientY-e.start.y)/e.viewport.zoom,a=(i.rotation||0)*Math.PI/180;if("move"===e.kind){const e=Number(this.getAttribute("snap-to-grid"))||0,round=i=>e>0&&!t.altKey?Math.round(i/e)*e:Math.round(i);s.x=round(i.x+o),s.y=round(i.y+n)}if("rotate"===e.kind){const o=this.point(t),n=Math.atan2(o.y-i.y-i.height/2,o.x-i.x-i.width/2),a=(i.rotation||0)+180*(n-e.angle)/Math.PI;s.rotation=t.shiftKey?15*Math.round(a/15):Math.round(a)}if("resize"===e.kind){const r=o*Math.cos(a)+n*Math.sin(a),h=-o*Math.sin(a)+n*Math.cos(a),d=e.handle;let l=clamp(i.width+(d.includes("e")?r:d.includes("w")?-r:0),16,8e3),c=clamp(i.height+(d.includes("s")?h:d.includes("n")?-h:0),16,8e3);if(t.shiftKey&&2===d.length){const t=Math.max(l/i.width,c/i.height),e=clamp(t,Math.max(16/i.width,16/i.height),Math.min(8e3/i.width,8e3/i.height));l=i.width*e,c=i.height*e}const u=(l-i.width)/2*(d.includes("w")?-1:d.includes("e")?1:0),p=(c-i.height)/2*(d.includes("n")?-1:d.includes("s")?1:0);s.width=Math.round(l),s.height=Math.round(c),s.x=i.x+i.width/2+u*Math.cos(a)-p*Math.sin(a)-s.width/2,s.y=i.y+i.height/2+u*Math.sin(a)+p*Math.cos(a)-s.height/2}this.data=this.data.map(t=>t.id===i.id?s:t),this.emit(e.kind)},this.onPointerUp=t=>{this.pointers.delete(t.pointerId),this.pointers.size<2&&(this.pinch=void 0);const e=this.gesture;e?.pointer===t.pointerId&&(this.gesture=void 0,"pan"===e.kind||same(e.before,this.data)||(this.remember(e.before),this.emit("commit"))),this.surface?.hasPointerCapture(t.pointerId)&&this.surface.releasePointerCapture(t.pointerId),this.drawViewport()},this.onPointerCancel=t=>{this.pointers.delete(t.pointerId),this.pinch=void 0,this.cancelGesture(),this.drawViewport()},this.onLostCapture=t=>{this.gesture?.pointer===t.pointerId&&this.cancelGesture()},this.onWheel=t=>{t.target.closest("db-context-bar")||this.gesture||this.pinch||(t.preventDefault(),t.ctrlKey||t.metaKey?this.zoomTo(this.view.zoom*Math.exp(.002*-t.deltaY),{x:t.clientX,y:t.clientY}):(this.view.x-=t.deltaX,this.view.y-=t.deltaY,this.emitViewport()))},this.onKeyUp=t=>{"Space"!==t.code&&" "!==t.key||(this.space=!1,this.drawViewport())},this.onBlur=()=>{this.space=!1,this.cancelGesture(),this.pointers.clear(),this.pinch=void 0,this.drawViewport()},this.onKeyDown=t=>{if(t.defaultPrevented||t.target.closest("db-tool-button,db-design-toolbar,db-context-bar"))return;if(this.editable(t))return;if("Space"===t.code||" "===t.key)return t.preventDefault(),this.space=!0,void this.drawViewport();if("Escape"===t.key)return t.preventDefault(),void(this.gesture?this.cancelGesture():this.select(null));if("h"!==t.key.toLowerCase()||t.ctrlKey||t.metaKey||(this.hand=!0,this.drawViewport()),"v"!==t.key.toLowerCase()||t.ctrlKey||t.metaKey||(this.hand=!1,this.drawViewport()),(t.ctrlKey||t.metaKey)&&"z"===t.key.toLowerCase())return t.preventDefault(),void(t.shiftKey?this.redo():this.undo());if((t.ctrlKey||t.metaKey)&&"y"===t.key.toLowerCase())return t.preventDefault(),void this.redo();if("+"===t.key||"="===t.key)return t.preventDefault(),void this.zoomTo(1.2*this.view.zoom);if("-"===t.key)return t.preventDefault(),void this.zoomTo(this.view.zoom/1.2);if("0"===t.key)return t.preventDefault(),void(t.shiftKey?this.zoomTo(1):this.fitToContent());if(!t.ctrlKey&&!t.metaKey&&!t.altKey){const e=t.key.toLowerCase();if(["t","r","o","l","f"].includes(e))return t.preventDefault(),void("t"===e?this.insertNode("text"):"f"===e?this.insertNode("frame","frame"):this.insertNode("shape","r"===e?"rectangle":"o"===e?"ellipse":"line"))}const e=this.data.find(t=>t.id===this.selected);if(e&&!e.locked&&!this.readOnly){if("Delete"===t.key||"Backspace"===t.key)return t.preventDefault(),this.remember(this.data),this.data=this.data.filter(t=>t.id!==e.id),this.select(null),void this.emit("delete");if(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(t.key)){t.preventDefault();const i=t.shiftKey?10:1,s={...e},o=t.target.dataset.handle;this.remember(this.data),"rotate"===o?s.rotation=(e.rotation||0)+("ArrowLeft"===t.key||"ArrowDown"===t.key?-i:i):o?"ArrowLeft"===t.key||"ArrowRight"===t.key?s.width=clamp(e.width+("ArrowRight"===t.key?i:-i),16,8e3):s.height=clamp(e.height+("ArrowDown"===t.key?i:-i),16,8e3):(s.x+="ArrowRight"===t.key?i:"ArrowLeft"===t.key?-i:0,s.y+="ArrowDown"===t.key?i:"ArrowUp"===t.key?-i:0),this.data=this.data.map(t=>t.id===e.id?s:t),this.emit("keyboard")}}},this.onDragOver=t=>{!this.readOnly&&t.dataTransfer?.types.includes("Files")&&(t.preventDefault(),t.dataTransfer.dropEffect="copy",this.surface?.classList.add("designbase-wc-design-canvas__surface--drop"))},this.onDragLeave=t=>{this.surface?.contains(t.relatedTarget)||this.surface?.classList.remove("designbase-wc-design-canvas__surface--drop")},this.onDrop=t=>{t.preventDefault(),this.surface?.classList.remove("designbase-wc-design-canvas__surface--drop"),!this.readOnly&&t.dataTransfer&&this.importImages(Array.from(t.dataTransfer.files),this.point(t))}}static get observedAttributes(){return["nodes","selected-id","min-zoom","max-zoom","readonly","snap-to-grid","grid-size","show-grid","height","max-file-size","toolbar-mode"]}get nodes(){return clone(this.data)}set nodes(t){try{const e="string"==typeof t?JSON.parse(t):t;if(!Array.isArray(e))return;const i=new Set,s=e.filter(t=>t&&"string"==typeof t.id&&t.id&&!i.has(t.id)&&!!i.add(t.id)).slice(0,100).map(t=>this.normalizeNode(t));if(same(this.data,s))return;this.data.length&&!this.gesture&&this.remember(this.data),this.data=s,this.data.some(t=>t.id===this.selected)||(this.selected=null),this.draw()}catch{}}get selectedId(){return this.selected}set selectedId(t){this.selected=this.data.some(e=>e.id===t)?t:null,this.drawSelection()}get viewport(){return{...this.view}}set viewport(t){this.view={x:num(t?.x,0),y:num(t?.y,0),zoom:clamp(num(t?.zoom,1),this.minZoom,this.maxZoom)},this.drawViewport()}get minZoom(){return clamp(Number(this.getAttribute("min-zoom"))||.1,.05,4)}get maxZoom(){return Math.max(this.minZoom,Math.min(8,Number(this.getAttribute("max-zoom"))||4))}get readOnly(){return this.hasAttribute("readonly")}set readOnly(t){this.toggleAttribute("readonly",t)}connectedCallback(){this.generation++,this.ensureStructure(),this.collectSlots(),this.observer=new MutationObserver(()=>{this.collectSlots(),this.draw()}),this.observer.observe(this,{childList:!0}),this.resizeObserver=new ResizeObserver(()=>this.drawViewport()),this.resizeObserver.observe(this.surface),window.addEventListener("keyup",this.onKeyUp),window.addEventListener("blur",this.onBlur),this.draw()}disconnectedCallback(){this.generation++,this.editing=void 0,this.observer?.disconnect(),this.resizeObserver?.disconnect(),window.removeEventListener("keyup",this.onKeyUp),window.removeEventListener("blur",this.onBlur),this.cancelGesture(),this.space=!1,this.pointers.clear(),this.pinch=void 0;for(const t of this.reading)t.abort();this.reading.clear()}attributeChangedCallback(t){"nodes"===t?this.nodes=this.getAttribute("nodes")||"[]":"selected-id"===t?this.selectedId=this.getAttribute("selected-id"):("readonly"===t&&this.cancelGesture(),this.view.zoom=clamp(this.view.zoom,this.minZoom,this.maxZoom),this.draw())}normalizeNode(t){return{...t,type:["frame","image","text","shape","component"].includes(t.type)?t.type:"frame",name:"string"==typeof t.name?t.name:"요소",x:num(t.x,0),y:num(t.y,0),width:clamp(num(t.width,320),16,8e3),height:clamp(num(t.height,240),16,8e3),rotation:num(t.rotation,0),flipX:!0===t.flipX,flipY:!0===t.flipY,opacity:clamp(num(t.opacity,1),0,1),radius:clamp(num(t.radius,0),0,4e3),text:"string"==typeof t.text?t.text.slice(0,1e4):"",fontSize:clamp(num(t.fontSize,24),8,240),fontWeight:clamp(num(t.fontWeight,400),100,900),textColor:t.textColor||"#191919",textAlign:["left","center","right"].includes(t.textAlign||"")?t.textAlign:"left",shape:["rectangle","ellipse","triangle","line"].includes(t.shape||"")?t.shape:"rectangle",component:["button","input","card","badge"].includes(t.component||"")?t.component:"button",stroke:t.stroke||"#191919",strokeWidth:clamp(num(t.strokeWidth,1),0,24)}}ensureStructure(){if(this.surface)return;this.classList.add("designbase-wc-design-canvas"),this.setAttribute("role","region"),this.hasAttribute("aria-label")||this.setAttribute("aria-label",t("디자인 캔버스")),this.toolbar=document.createElement("db-design-toolbar"),this.toolbar.classList.add("designbase-wc-design-canvas__toolbar"),this.toolbar.setAttribute("aria-label",t("캔버스 도구")),this.toolbar.setAttribute("size","s"),this.toolbar.addEventListener("db-tool-action",this.toolAction),this.input=document.createElement("input"),this.input.type="file",this.input.multiple=!0,this.input.accept="image/png,image/jpeg,image/webp,image/gif",this.input.hidden=!0,this.input.addEventListener("change",()=>{this.importImages(Array.from(this.input.files||[])),this.input.value=""}),this.surface=document.createElement("div"),this.surface.className="designbase-wc-design-canvas__surface",this.surface.tabIndex=0,this.surface.setAttribute("aria-label",t("캔버스 작업 영역")),this.surface.setAttribute("aria-describedby",this.id?`${this.id}-help`:""),this.world=document.createElement("div"),this.world.className="designbase-wc-design-canvas__world",this.selection=document.createElement("div"),this.selection.className="designbase-wc-design-canvas__selection";for(const t of["nw","n","ne","e","se","s","sw","w","rotate"]){const e=document.createElement("div");e.dataset.handle=t,e.tabIndex=0,e.setAttribute("role","button"),e.setAttribute("aria-label","rotate"===t?"선택 요소 회전":`선택 요소 크기 조정 ${t}`),e.className="designbase-wc-design-canvas__handle",e.title="rotate"===t?"회전 · Shift: 15° 단위":"크기 조정 · Shift: 비율 유지",this.selection.append(e)}const e=document.createElement("span");e.className="designbase-wc-design-canvas__dimensions",this.selection.append(e),this.world.append(this.selection),this.contextBar=document.createElement("db-context-bar"),this.contextBar.classList.add("designbase-wc-design-canvas__context"),this.contextBar.setAttribute("size","s"),this.contextBar.setAttribute("tone","dark"),this.contextBar.setAttribute("placement","top-start"),this.contextBar.addEventListener("db-tool-action",this.toolAction),this.surface.append(this.world,this.contextBar),this.status=document.createElement("div"),this.status.className="designbase-wc-design-canvas__status",this.status.setAttribute("role","status"),this.status.setAttribute("aria-live","polite"),this.status.textContent=t("Space + 드래그 이동 · Ctrl/⌘ + 휠 확대 · 이미지 드롭"),this.surface.addEventListener("pointerdown",this.onPointerDown),this.surface.addEventListener("pointermove",this.onPointerMove),this.surface.addEventListener("pointerup",this.onPointerUp),this.surface.addEventListener("pointercancel",this.onPointerCancel),this.surface.addEventListener("lostpointercapture",this.onLostCapture),this.surface.addEventListener("wheel",this.onWheel,{passive:!1}),this.surface.addEventListener("keydown",this.onKeyDown),this.surface.addEventListener("dblclick",this.beginTextEdit),this.surface.addEventListener("dragover",this.onDragOver),this.surface.addEventListener("dragleave",this.onDragLeave),this.surface.addEventListener("drop",this.onDrop),this.surface.addEventListener("dragstart",t=>t.preventDefault()),this.append(this.toolbar,this.surface,this.status,this.input)}collectSlots(){for(const t of Array.from(this.children))t instanceof HTMLElement&&t.hasAttribute("slot")&&this.slots.set(t.getAttribute("slot"),t)}draw(){if(this.world&&this.surface){if(this.style.height=this.getAttribute("height")||"560px",this.toolbar){const t="floating"===this.getAttribute("toolbar-mode");this.toolbar.toggleAttribute("movable",t),this.toolbar.classList.toggle("designbase-wc-design-canvas__toolbar--floating",t),t&&this.toolbar.parentElement!==this.surface&&this.surface.append(this.toolbar),t||this.toolbar.parentElement===this||this.insertBefore(this.toolbar,this.surface)}this.surface.classList.toggle("designbase-wc-design-canvas__surface--no-grid","false"===this.getAttribute("show-grid"));for(const[t,e]of this.objectElements)if(!this.data.some(e=>e.id===t)){const i=e.querySelector("[slot]");i instanceof HTMLElement&&this.slots.set(i.slot,i),e.remove(),this.objectElements.delete(t)}for(const t of this.data){let e=this.objectElements.get(t.id);if(e||(e=document.createElement("div"),e.className="designbase-wc-design-canvas__node",e.dataset.nodeId=t.id,e.tabIndex=-1,this.world.insertBefore(e,this.selection),this.objectElements.set(t.id,e)),e.style.zIndex=String(this.data.indexOf(t)),e.dataset.nodeType=t.type,e.setAttribute("aria-label",t.name),e.setAttribute("role","group"),Object.assign(e.style,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.width}px`,height:`${t.height}px`,transform:`rotate(${t.rotation||0}deg) scale(${t.flipX?-1:1}, ${t.flipY?-1:1})`,opacity:String(t.opacity??1),borderRadius:`${t.radius||0}px`,background:t.fill||"var(--db-surface-base)"}),"text"===t.type)this.drawText(e,t);else if("shape"===t.type)this.drawShape(e,t);else if("component"===t.type)this.drawComponent(e,t);else if("image"===t.type){let i=e.querySelector("img");i||(i=document.createElement("img"),i.draggable=!1,e.replaceChildren(i)),i.alt=t.name;const s=t.src||"",o=/^(data:image\/(png|jpe?g|webp|gif);base64,|blob:|https?:\/\/|\/[^/])/i.test(s);o&&i.getAttribute("src")!==s&&(i.src=s),o||i.removeAttribute("src")}else if(t.slot&&this.slots.has(t.slot)){const i=this.slots.get(t.slot);i.parentElement!==e&&e.replaceChildren(i)}else t.slot||e.textContent===t.name||(e.textContent=t.name)}this.drawViewport(),this.drawSelection()}}drawText(t,e){let i=t.querySelector(".designbase-wc-design-canvas__text");i||(i=document.createElement("div"),i.className="designbase-wc-design-canvas__text",t.replaceChildren(i)),t.style.background="transparent",t.style.overflow="visible",i.isContentEditable||i.textContent===e.text||(i.textContent=e.text||""),Object.assign(i.style,{fontSize:`${e.fontSize}px`,fontWeight:String(e.fontWeight),color:e.textColor,textAlign:e.textAlign})}drawShape(t,e){t.style.background="transparent";const i="http://www.w3.org/2000/svg",s=e.shape||"rectangle";if(t.dataset.shape!==s||!t.querySelector("svg")){t.dataset.shape=s;const e=document.createElementNS(i,"svg");e.setAttribute("viewBox","0 0 100 100"),e.setAttribute("preserveAspectRatio","none"),e.setAttribute("aria-hidden","true");const o=document.createElementNS(i,"ellipse"===s?"ellipse":"rectangle"===s?"rect":"line"===s?"line":"polygon");e.append(o),t.replaceChildren(e)}const o=t.querySelector("svg").firstElementChild;"rectangle"===s&&(o.setAttribute("x","1"),o.setAttribute("y","1"),o.setAttribute("width","98"),o.setAttribute("height","98"),o.setAttribute("rx",String((e.radius||0)/e.width*100)),o.setAttribute("ry",String((e.radius||0)/e.height*100))),"ellipse"===s&&(o.setAttribute("cx","50"),o.setAttribute("cy","50"),o.setAttribute("rx","49"),o.setAttribute("ry","49")),"triangle"===s&&o.setAttribute("points","50,1 99,99 1,99"),"line"===s&&(o.setAttribute("x1","0"),o.setAttribute("y1","50"),o.setAttribute("x2","100"),o.setAttribute("y2","50")),o.setAttribute("fill","line"===s?"none":e.fill||"#dce8f5"),o.setAttribute("stroke",e.stroke||"#191919"),o.setAttribute("stroke-width",String(e.strokeWidth??1)),o.setAttribute("vector-effect","non-scaling-stroke")}drawComponent(e,i){const s=i.component||"button";let o=e.querySelector(".designbase-wc-design-canvas__component");if(!o||o.dataset.kind!==s){o=document.createElement("div"),o.className="designbase-wc-design-canvas__component",o.inert=!0,o.dataset.kind=s;const i=document.createElement("input"===s?"db-input":"card"===s?"db-card":"badge"===s?"db-badge":"db-button");if("button"===s&&i.setAttribute("variant","primary"),"card"===s){const e=document.createElement("h3");e.dataset.componentText="";const s=document.createElement("p");s.textContent=t("내용을 선택하고 나만의 화면으로 구성하세요."),i.append(e,s)}o.append(i),e.replaceChildren(o)}const n=o.firstElementChild;if("input"===s){const t=n.querySelector("input");t&&Object.assign(t.style,{color:i.textColor,fontSize:`${i.fontSize}px`})}if("input"===s)n.setAttribute("placeholder",i.text||"내용을 입력하세요");else if("card"===s){const t=n.querySelector("[data-component-text]");t.textContent=i.text||"새 카드",t.style.fontSize=`${i.fontSize}px`}else n.textContent!==i.text&&(n.textContent=i.text||("badge"===s?"Badge":"Button"));Object.assign(n.style,{width:"100%",height:"100%",boxSizing:"border-box",borderRadius:`${i.radius||0}px`,color:i.textColor,fontSize:`${i.fontSize}px`,background:i.fill||"#ffffff"})}drawViewport(){if(!this.world||!this.surface)return;this.world.style.transform=`translate(${this.surface.clientWidth/2+this.view.x}px, ${this.surface.clientHeight/2+this.view.y}px) scale(${this.view.zoom})`,this.style.setProperty("--db-canvas-inverse-zoom",String(1/this.view.zoom));const t=Math.max(4,Number(this.getAttribute("grid-size"))||24)*this.view.zoom;this.surface.style.backgroundSize=`${t}px ${t}px`,this.surface.style.backgroundPosition=`${this.surface.clientWidth/2+this.view.x}px ${this.surface.clientHeight/2+this.view.y}px`,this.surface.classList.toggle("designbase-wc-design-canvas__surface--pan",this.space||this.hand||"pan"===this.gesture?.kind),this.syncToolbar(),this.positionContext()}drawSelection(){if(!this.selection)return;const t=this.data.find(t=>t.id===this.selected);if(this.selection.hidden=!t,this.syncContext(),this.positionContext(),t){Object.assign(this.selection.style,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.width}px`,height:`${t.height}px`,transform:`rotate(${t.rotation||0}deg)`}),this.selection.classList.toggle("designbase-wc-design-canvas__selection--locked",Boolean(t.locked||this.readOnly)),this.selection.querySelector(".designbase-wc-design-canvas__dimensions").textContent=`${Math.round(t.width)} × ${Math.round(t.height)} · ${Math.round(t.rotation||0)}°`;for(const e of Array.from(this.selection.querySelectorAll("[data-handle]")))e.tabIndex=t.locked||this.readOnly?-1:0,e.setAttribute("aria-disabled",String(!!t.locked||this.readOnly))}}syncToolbar(){if(!this.toolbar)return;const e=Math.round(100*this.view.zoom),i=[{id:"select",label:t("선택 도구"),icon:"navigation",shortcut:"V",active:!this.hand},{id:"pan",label:t("화면 이동"),icon:"move",shortcut:"H / Space",active:this.hand},{id:"creation-separator",type:"separator"},{id:"frame",label:t("프레임 추가"),icon:"file-new",disabled:this.readOnly||this.data.length>=100,menuTrigger:"long-press",value:this.creationTool,options:[{id:"frame",label:t("빈 프레임 추가"),icon:"file-new"},{id:"note",label:t("메모 추가"),icon:"documentation"}]},{id:"text",label:t("텍스트 추가"),icon:"text",shortcut:"T",disabled:this.readOnly||this.data.length>=100},{id:"shape",label:t("도형 추가"),icon:"shapes",value:this.shapeTool,disabled:this.readOnly||this.data.length>=100,options:[{id:"rectangle",label:t("사각형"),icon:"shape-rectangle",shortcut:"R"},{id:"ellipse",label:t("타원"),icon:"shape-ellipse",shortcut:"O"},{id:"triangle",label:t("삼각형"),icon:"shape-triangle"},{id:"line",label:t("선"),icon:"shape-line",shortcut:"L"}]},{id:"component",label:t("컴포넌트 추가"),icon:"package",value:this.componentTool,disabled:this.readOnly||this.data.length>=100,options:[{id:"button",label:t("버튼"),icon:"package"},{id:"input",label:t("텍스트 입력"),icon:"documentation"},{id:"card",label:t("카드"),icon:"file-new"},{id:"badge",label:t("배지"),icon:"package"}]},{id:"image",label:t("이미지 추가"),icon:"gallery",disabled:this.readOnly||this.data.length>=100},{id:"history-separator",type:"separator"},{id:"undo",label:t("실행 취소"),icon:"arrow-back-up",shortcut:"⌘/Ctrl+Z",disabled:!this.past.length||this.readOnly},{id:"redo",label:t("다시 실행"),icon:"arrow-back-up",shortcut:"⌘/Ctrl+Shift+Z",disabled:!this.future.length||this.readOnly},{id:"zoom-separator",type:"separator"},{id:"zoom-out",label:t("축소"),icon:"zoom-out",shortcut:"−"},{id:"zoom",label:t("확대율"),text:`${e}%`,value:String(e),options:[...[25,50,100,200,400].filter(t=>t/100>=this.minZoom&&t/100<=this.maxZoom).map(t=>({id:String(t),label:`${t}%`})),{id:"fit",label:t("전체 보기"),icon:"expand",shortcut:"0"}]},{id:"zoom-in",label:t("확대"),icon:"zoom-in",shortcut:"+"},{id:"fit",label:t("전체 보기"),icon:"expand",shortcut:"0"}];this.toolbar.items=i}syncContext(){if(!this.contextBar)return;const e=this.data.find(t=>t.id===this.selected);if(this.contextBar.hidden=!e,!e)return;const i=this.readOnly||!!e.locked;this.contextBar.items=[...e.slot?[]:[{id:"duplicate",label:t("요소 복제"),icon:"copy",disabled:i||this.data.length>=100}],{id:"lock",label:e.locked?"잠금 해제":"요소 잠그기",icon:e.locked?"unlock":"lock",active:e.locked,disabled:this.readOnly},{id:"transform-separator",type:"separator"},{id:"rotate",label:t("회전"),icon:"refresh",disabled:i,options:[{id:"reset",label:t("회전 초기화")},{id:"left",label:t("왼쪽으로 90° 회전")},{id:"right",label:t("오른쪽으로 90° 회전")}]},{id:"flip-x",label:t("좌우 반전"),icon:"filp-horizontal",active:!!e.flipX,disabled:i},{id:"flip-y",label:t("상하 반전"),icon:"filp-vertical",active:!!e.flipY,disabled:i},{id:"alignment-separator",type:"separator"},...[["left","화면 왼쪽에 정렬","layout-align-left"],["center","가로 중앙 정렬","layout-align-center"],["right","화면 오른쪽에 정렬","layout-align-right"],["top","화면 위쪽에 정렬","layout-align-top"],["middle","세로 중앙 정렬","layout-align-middle"],["bottom","화면 아래쪽에 정렬","layout-align-bottom"]].map(([t,e,s])=>({id:`align-${t}`,label:e,icon:s,disabled:i})),{id:"layer-separator",type:"separator"},{id:"arrange",label:t("레이어 순서"),icon:"layer",disabled:i,options:[{id:"front",label:t("맨 앞으로 가져오기")},{id:"back",label:t("맨 뒤로 보내기")}]},{id:"delete",label:t("요소 삭제"),icon:"trash",shortcut:"Delete",disabled:i}]}insertNode(t,e){if(this.readOnly||this.data.length>=100)return;this.remember(this.data),"frame"===t&&(this.creationTool=e||this.creationTool),"shape"===t&&(this.shapeTool=e||this.shapeTool),"component"===t&&(this.componentTool=e||this.componentTool);const i="frame"===t&&"note"===this.creationTool;let s=320,o=240,n=i?"새로운 메모":"새 프레임",a="";if(i&&(s=220,o=180),"text"===t&&(s=280,o=100,n="텍스트",a="새로운 아이디어"),"shape"===t&&(s=200,o="line"===this.shapeTool?24:160,n={rectangle:"사각형",ellipse:"타원",triangle:"삼각형",line:"선"}[this.shapeTool]),"component"===t){const t={button:[160,44,"Button"],input:[240,44,"내용을 입력하세요"],card:[280,180,"새 카드"],badge:[100,32,"Badge"]};[s,o,a]=t[this.componentTool]||t.button,n=`${a} 컴포넌트`}const r=this.normalizeNode({id:crypto.randomUUID(),type:t,name:n,x:-this.view.x/this.view.zoom-s/2,y:-this.view.y/this.view.zoom-o/2,width:s,height:o,radius:"component"===t?10:i?12:0,fill:"shape"===t?"#dce8f5":"component"===t&&"button"===this.componentTool?"#191919":i?"#edf2e9":"#ffffff",text:a,fontSize:"component"===t?"card"===this.componentTool?22:14:24,textColor:"component"===t&&"button"===this.componentTool?"#ffffff":"#191919",shape:this.shapeTool,component:this.componentTool});this.data=[...this.data,r],this.select(r.id),this.emit("create")}positionContext(){const t=this.data.find(t=>t.id===this.selected);if(!t||!this.surface||!this.contextBar)return;const e=this.contextBar.offsetWidth||200,i=this.contextBar.offsetHeight||40,s=(t.rotation||0)*Math.PI/180,o=(Math.abs(t.width*Math.sin(s))+Math.abs(t.height*Math.cos(s)))/2*this.view.zoom,n=this.surface.clientWidth/2+this.view.x+(t.x+t.width/2)*this.view.zoom,a=this.surface.clientHeight/2+this.view.y+(t.y+t.height/2)*this.view.zoom,r=a-o-i-44,h=a+o+36,d=r>=12?r:h+i<=this.surface.clientHeight-12?h:12;this.contextBar.style.left=`${clamp(n,e/2+12,Math.max(e/2+12,this.surface.clientWidth-e/2-12))}px`,this.contextBar.style.top=`${clamp(d,12,Math.max(12,this.surface.clientHeight-i-12))}px`}select(t){t!==this.selected&&(this.selected=t,this.drawSelection(),this.dispatchEvent(new CustomEvent("db-selection-change",{detail:{selectedId:t,node:this.data.find(e=>e.id===t)||null},bubbles:!0,composed:!0})))}emit(t){this.draw(),this.dispatchEvent(new CustomEvent("db-nodes-change",{detail:{nodes:this.nodes,selectedId:this.selected,reason:t},bubbles:!0,composed:!0}))}emitViewport(){this.drawViewport(),this.dispatchEvent(new CustomEvent("db-viewport-change",{detail:this.viewport,bubbles:!0,composed:!0}))}remember(t){this.past.push(clone(t)),this.past.length>40&&this.past.shift(),this.future=[]}undo(){!this.readOnly&&this.past.length&&(this.cancelGesture(),this.future.push(this.nodes),this.data=this.past.pop(),this.select(this.data.some(t=>t.id===this.selected)?this.selected:this.data[0]?.id||null),this.emit("undo"))}redo(){!this.readOnly&&this.future.length&&(this.cancelGesture(),this.past.push(this.nodes),this.data=this.future.pop(),this.select(this.data.some(t=>t.id===this.selected)?this.selected:this.data[0]?.id||null),this.emit("redo"))}point(t){const e=this.surface.getBoundingClientRect();return{x:(t.clientX-e.left-this.surface.clientWidth/2-this.view.x)/this.view.zoom,y:(t.clientY-e.top-this.surface.clientHeight/2-this.view.y)/this.view.zoom}}zoomTo(t,e){if(!this.surface)return;const i=this.surface.getBoundingClientRect(),s=e||{x:i.left+i.width/2,y:i.top+i.height/2},o=this.point({clientX:s.x,clientY:s.y});this.view.zoom=clamp(t,this.minZoom,this.maxZoom),this.view.x=s.x-i.left-this.surface.clientWidth/2-o.x*this.view.zoom,this.view.y=s.y-i.top-this.surface.clientHeight/2-o.y*this.view.zoom,this.emitViewport(),this.drawSelection()}fitToContent(){if(!this.surface)return;if(!this.data.length)return this.viewport={x:0,y:0,zoom:1},void this.emitViewport();const t=this.data.flatMap(t=>{const e=(t.rotation||0)*Math.PI/180,i=t.x+t.width/2,s=t.y+t.height/2;return[[-1,-1],[1,-1],[1,1],[-1,1]].map(([o,n])=>({x:i+o*t.width/2*Math.cos(e)-n*t.height/2*Math.sin(e),y:s+o*t.width/2*Math.sin(e)+n*t.height/2*Math.cos(e)}))}),e=Math.min(...t.map(t=>t.x)),i=Math.max(...t.map(t=>t.x)),s=Math.min(...t.map(t=>t.y)),o=Math.max(...t.map(t=>t.y)),n=clamp(Math.min((this.surface.clientWidth-96)/Math.max(16,i-e),(this.surface.clientHeight-96)/Math.max(16,o-s)),this.minZoom,Math.min(1,this.maxZoom));this.view={zoom:n,x:-(e+i)/2*n,y:-(s+o)/2*n},this.emitViewport(),this.drawSelection()}editable(t){return t.composedPath().some(t=>t instanceof HTMLElement&&(t.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))}cancelGesture(){const t=this.gesture;this.gesture=void 0,t&&"pan"!==t.kind&&(this.data=t.before,this.isConnected&&this.emit("cancel")),t&&this.surface?.hasPointerCapture(t.pointer)&&this.surface.releasePointerCapture(t.pointer)}async importImages(t,i){if(this.readOnly)return;const s=i||{x:-this.view.x/this.view.zoom,y:-this.view.y/this.view.zoom},o=this.generation,n=[];let a=0;const r=Math.max(1,Number(this.getAttribute("max-file-size"))||10485760);for(const e of t.slice(0,12))if(!["image/png","image/jpeg","image/webp","image/gif"].includes(e.type)||e.size>r||this.data.length+n.length>=100)a++;else try{const t=await new Promise((t,i)=>{const s=new FileReader;this.reading.add(s),s.onload=()=>{this.reading.delete(s),t(String(s.result))},s.onerror=s.onabort=()=>{this.reading.delete(s),i(new Error("이미지를 읽지 못했습니다."))},s.readAsDataURL(e)}),i=await new Promise((e,i)=>{const s=new Image;s.onload=()=>e({width:s.naturalWidth,height:s.naturalHeight}),s.onerror=i,s.src=t});if(!this.isConnected||o!==this.generation)return;const a=Math.min(1,480/Math.max(i.width,i.height));n.push({id:crypto.randomUUID(),type:"image",name:e.name,src:t,x:s.x+24*n.length-i.width*a/2,y:s.y+24*n.length-i.height*a/2,width:Math.max(16,i.width*a),height:Math.max(16,i.height*a),rotation:0,opacity:1,radius:0})}catch{a++}this.isConnected&&o===this.generation&&(n.length&&(this.remember(this.data),this.data=[...this.data,...n.map(t=>this.normalizeNode(t))],this.select(n[n.length-1].id),this.emit("import")),this.status&&(this.status.textContent=e("{0}개 이미지 추가{1}",[n.length,a||t.length>12?` · ${a+Math.max(0,t.length-12)}개 제외 (PNG/JPG/WebP/GIF, 파일당 ${Math.round(r/1024/1024)}MB 이하)`:""])))}}customElements.get("db-design-canvas")||customElements.define("db-design-canvas",DbDesignCanvas);export{DbDesignCanvas};
|
|
1
|
+
import{dbText as t,dbFormat as e}from"../core/i18n.js";import"./db-button.js";import"./db-input.js";import"./db-card.js";import"./db-badge.js";const clamp=(t,e,i)=>Math.min(i,Math.max(e,t)),num=(t,e)=>"number"==typeof t&&Number.isFinite(t)?t:e,clone=t=>t.map(t=>({...t})),same=(t,e)=>t.length===e.length&&t.every((t,i)=>Object.keys({...t,...e[i]}).every(s=>t[s]===e[i][s]));class DbDesignCanvas extends HTMLElement{constructor(){super(...arguments),this.data=[],this.selected=null,this.view={x:0,y:0,zoom:1},this.objectElements=new Map,this.slots=new Map,this.pointers=new Map,this.space=!1,this.hand=!1,this.creationTool="frame",this.shapeTool="rectangle",this.componentTool="button",this.past=[],this.future=[],this.reading=new Set,this.generation=0,this.beginTextEdit=e=>{if(this.hand||this.space)return;const i=e.target.closest("[data-node-id]")||document.elementFromPoint(e.clientX,e.clientY)?.closest("[data-node-id]"),s=this.data.find(t=>t.id===i?.dataset.nodeId);if(!s||"text"!==s.type||s.locked||this.readOnly)return;const o=i.querySelector(".designbase-wc-design-canvas__text");if(o.isContentEditable)return;e.preventDefault(),this.editing={id:s.id,before:this.nodes},o.contentEditable="plaintext-only",o.setAttribute("role","textbox"),o.setAttribute("aria-label",t("텍스트 편집")),o.tabIndex=0,o.focus();const finish=(t=!1)=>{if(this.editing?.id!==s.id)return;const e=this.editing.before;this.editing=void 0,o.removeEventListener("input",input),o.removeEventListener("keydown",key),o.removeEventListener("blur",blur),o.contentEditable="false",o.removeAttribute("role"),o.removeAttribute("aria-label"),o.tabIndex=-1,t?this.data=e:same(e,this.data)||this.remember(e),this.emit(t?"text-cancel":"text")},input=()=>{const t=(o.innerText||"").slice(0,1e4);this.data=this.data.map(e=>e.id===s.id?{...e,text:t}:e),this.emit("text-input")},key=t=>{("Escape"===t.key||"Enter"===t.key&&(t.ctrlKey||t.metaKey))&&(t.preventDefault(),t.stopPropagation(),finish("Escape"===t.key),o.blur(),this.surface?.focus({preventScroll:!0}))},blur=()=>finish();o.addEventListener("input",input),o.addEventListener("keydown",key),o.addEventListener("blur",blur)},this.toolAction=t=>{t.stopPropagation();const{id:e,optionId:i}=t.detail;if("select"===e||"pan"===e)return this.hand="pan"===e,void this.drawViewport();if("image"===e)return void(this.readOnly||this.input?.click());if("undo"===e)return void this.undo();if("redo"===e)return void this.redo();if("zoom-in"===e)return void this.zoomTo(1.2*this.view.zoom);if("zoom-out"===e)return void this.zoomTo(this.view.zoom/1.2);if("fit"===e||"zoom"===e&&"fit"===i)return void this.fitToContent();if("zoom"===e){const t=Number(i)/100;return void(t>0&&this.zoomTo(t))}if(this.readOnly)return;if(["frame","text","shape","component"].includes(e))return void this.insertNode(e,i);const s=this.data.find(t=>t.id===this.selected);if(s&&(!s.locked||"lock"===e)&&("duplicate"!==e||!(s.slot||this.data.length>=100))){if(this.remember(this.data),"duplicate"===e){const t={...s,id:crypto.randomUUID(),name:`${s.name} 복사`,x:s.x+24,y:s.y+24};this.data=[...this.data,t],this.select(t.id)}else if("delete"===e)this.data=this.data.filter(t=>t.id!==s.id),this.select(null);else if("arrange"===e){const t=this.data.filter(t=>t.id!==s.id);this.data="front"===i?[...t,s]:[s,...t]}else{const t={...s};if("lock"===e&&(t.locked=!s.locked),"rotate"===e&&(t.rotation="reset"===i?0:(s.rotation||0)+("left"===i?-90:90)),"flip-x"===e&&(t.flipX=!s.flipX),"flip-y"===e&&(t.flipY=!s.flipY),("align"===e||e.startsWith("align-"))&&this.surface){const o="align"===e?i:e.slice(6),n=-this.view.x/this.view.zoom,a=-this.view.y/this.view.zoom,r=this.surface.clientWidth/this.view.zoom/2,h=this.surface.clientHeight/this.view.zoom/2,d=(s.rotation||0)*Math.PI/180,l=Math.abs(s.width*Math.cos(d))+Math.abs(s.height*Math.sin(d)),c=Math.abs(s.width*Math.sin(d))+Math.abs(s.height*Math.cos(d));"center"===o&&(t.x=n-s.width/2),"middle"===o&&(t.y=a-s.height/2),"left"===o&&(t.x=n-r+(l-s.width)/2+24),"right"===o&&(t.x=n+r-(l+s.width)/2-24),"top"===o&&(t.y=a-h+(c-s.height)/2+24),"bottom"===o&&(t.y=a+h-(c+s.height)/2-24)}this.data=this.data.map(e=>e.id===s.id?t:e)}this.emit(e)}},this.onPointerDown=t=>{if(t.target.closest("db-tool-button,db-design-toolbar,db-context-bar"))return;if(0!==t.button&&1!==t.button)return;if(this.pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),"touch"===t.pointerType&&2===this.pointers.size){this.cancelGesture();const[e,i]=[...this.pointers.values()],s={clientX:(e.x+i.x)/2,clientY:(e.y+i.y)/2};return this.pinch={distance:Math.hypot(e.x-i.x,e.y-i.y),zoom:this.view.zoom,world:this.point(s)},void this.surface.setPointerCapture(t.pointerId)}if(this.editable(t))return;const e=t.target,i=e.closest("[data-handle]")?.dataset.handle,s=e.closest("[data-node-id]")?.dataset.nodeId;s&&this.select(s);const o=this.data.find(t=>t.id===this.selected),n=this.space||this.hand||1===t.button;if(!n&&!i&&!s)return this.surface.focus({preventScroll:!0}),this.surface.setPointerCapture(t.pointerId),void this.select(null);if((n||o&&!o.locked&&!this.readOnly)&&(n||!e.closest("db-button,button,a,input,textarea,select")||i)){if(t.preventDefault(),this.surface.focus({preventScroll:!0}),this.gesture={kind:n?"pan":"rotate"===i?"rotate":i?"resize":"move",pointer:t.pointerId,start:{x:t.clientX,y:t.clientY},viewport:this.viewport,node:o?{...o}:void 0,handle:i,before:this.nodes},"rotate"===this.gesture.kind&&o){const e=this.point(t);this.gesture.angle=Math.atan2(e.y-o.y-o.height/2,e.x-o.x-o.width/2)}this.surface.setPointerCapture(t.pointerId),this.drawViewport()}},this.onPointerMove=t=>{if(this.pointers.has(t.pointerId)&&this.pointers.set(t.pointerId,{x:t.clientX,y:t.clientY}),this.pinch&&this.pointers.size>=2){const[t,e]=[...this.pointers.values()],i=this.surface.getBoundingClientRect();return this.view.zoom=clamp(this.pinch.zoom*Math.hypot(t.x-e.x,t.y-e.y)/Math.max(1,this.pinch.distance),this.minZoom,this.maxZoom),this.view.x=(t.x+e.x)/2-i.left-this.surface.clientWidth/2-this.pinch.world.x*this.view.zoom,this.view.y=(t.y+e.y)/2-i.top-this.surface.clientHeight/2-this.pinch.world.y*this.view.zoom,void this.emitViewport()}const e=this.gesture;if(!e||e.pointer!==t.pointerId)return;if("pan"===e.kind)return this.view.x=e.viewport.x+t.clientX-e.start.x,this.view.y=e.viewport.y+t.clientY-e.start.y,void this.emitViewport();const i=e.node;let s={...i};const o=(t.clientX-e.start.x)/e.viewport.zoom,n=(t.clientY-e.start.y)/e.viewport.zoom,a=(i.rotation||0)*Math.PI/180;if("move"===e.kind){const e=Number(this.getAttribute("snap-to-grid"))||0,round=i=>e>0&&!t.altKey?Math.round(i/e)*e:Math.round(i);s.x=round(i.x+o),s.y=round(i.y+n)}if("rotate"===e.kind){const o=this.point(t),n=Math.atan2(o.y-i.y-i.height/2,o.x-i.x-i.width/2),a=(i.rotation||0)+180*(n-e.angle)/Math.PI;s.rotation=t.shiftKey?15*Math.round(a/15):Math.round(a)}if("resize"===e.kind){const r=o*Math.cos(a)+n*Math.sin(a),h=-o*Math.sin(a)+n*Math.cos(a),d=e.handle;let l=clamp(i.width+(d.includes("e")?r:d.includes("w")?-r:0),16,8e3),c=clamp(i.height+(d.includes("s")?h:d.includes("n")?-h:0),16,8e3);if(t.shiftKey&&2===d.length){const t=Math.max(l/i.width,c/i.height),e=clamp(t,Math.max(16/i.width,16/i.height),Math.min(8e3/i.width,8e3/i.height));l=i.width*e,c=i.height*e}const u=(l-i.width)/2*(d.includes("w")?-1:d.includes("e")?1:0),p=(c-i.height)/2*(d.includes("n")?-1:d.includes("s")?1:0);s.width=Math.round(l),s.height=Math.round(c),s.x=i.x+i.width/2+u*Math.cos(a)-p*Math.sin(a)-s.width/2,s.y=i.y+i.height/2+u*Math.sin(a)+p*Math.cos(a)-s.height/2}this.data=this.data.map(t=>t.id===i.id?s:t),this.emit(e.kind)},this.onPointerUp=t=>{this.pointers.delete(t.pointerId),this.pointers.size<2&&(this.pinch=void 0);const e=this.gesture;e?.pointer===t.pointerId&&(this.gesture=void 0,"pan"===e.kind||same(e.before,this.data)||(this.remember(e.before),this.emit("commit"))),this.surface?.hasPointerCapture(t.pointerId)&&this.surface.releasePointerCapture(t.pointerId),this.drawViewport()},this.onPointerCancel=t=>{this.pointers.delete(t.pointerId),this.pinch=void 0,this.cancelGesture(),this.drawViewport()},this.onLostCapture=t=>{this.gesture?.pointer===t.pointerId&&this.cancelGesture()},this.onWheel=t=>{t.target.closest("db-context-bar")||this.gesture||this.pinch||(t.preventDefault(),t.ctrlKey||t.metaKey?this.zoomTo(this.view.zoom*Math.exp(.002*-t.deltaY),{x:t.clientX,y:t.clientY}):(this.view.x-=t.deltaX,this.view.y-=t.deltaY,this.emitViewport()))},this.onKeyUp=t=>{"Space"!==t.code&&" "!==t.key||(this.space=!1,this.drawViewport())},this.onBlur=()=>{this.space=!1,this.cancelGesture(),this.pointers.clear(),this.pinch=void 0,this.drawViewport()},this.onKeyDown=t=>{if(t.defaultPrevented||t.target.closest("db-tool-button,db-design-toolbar,db-context-bar"))return;if(this.editable(t))return;if("Space"===t.code||" "===t.key)return t.preventDefault(),this.space=!0,void this.drawViewport();if("Escape"===t.key)return t.preventDefault(),void(this.gesture?this.cancelGesture():this.select(null));if("h"!==t.key.toLowerCase()||t.ctrlKey||t.metaKey||(this.hand=!0,this.drawViewport()),"v"!==t.key.toLowerCase()||t.ctrlKey||t.metaKey||(this.hand=!1,this.drawViewport()),(t.ctrlKey||t.metaKey)&&"z"===t.key.toLowerCase())return t.preventDefault(),void(t.shiftKey?this.redo():this.undo());if((t.ctrlKey||t.metaKey)&&"y"===t.key.toLowerCase())return t.preventDefault(),void this.redo();if("+"===t.key||"="===t.key)return t.preventDefault(),void this.zoomTo(1.2*this.view.zoom);if("-"===t.key)return t.preventDefault(),void this.zoomTo(this.view.zoom/1.2);if("0"===t.key)return t.preventDefault(),void(t.shiftKey?this.zoomTo(1):this.fitToContent());if(!t.ctrlKey&&!t.metaKey&&!t.altKey){const e=t.key.toLowerCase();if(["t","r","o","l","f"].includes(e))return t.preventDefault(),void("t"===e?this.insertNode("text"):"f"===e?this.insertNode("frame","frame"):this.insertNode("shape","r"===e?"rectangle":"o"===e?"ellipse":"line"))}const e=this.data.find(t=>t.id===this.selected);if(e&&!e.locked&&!this.readOnly){if("Delete"===t.key||"Backspace"===t.key)return t.preventDefault(),this.remember(this.data),this.data=this.data.filter(t=>t.id!==e.id),this.select(null),void this.emit("delete");if(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"].includes(t.key)){t.preventDefault();const i=t.shiftKey?10:1,s={...e},o=t.target.dataset.handle;this.remember(this.data),"rotate"===o?s.rotation=(e.rotation||0)+("ArrowLeft"===t.key||"ArrowDown"===t.key?-i:i):o?"ArrowLeft"===t.key||"ArrowRight"===t.key?s.width=clamp(e.width+("ArrowRight"===t.key?i:-i),16,8e3):s.height=clamp(e.height+("ArrowDown"===t.key?i:-i),16,8e3):(s.x+="ArrowRight"===t.key?i:"ArrowLeft"===t.key?-i:0,s.y+="ArrowDown"===t.key?i:"ArrowUp"===t.key?-i:0),this.data=this.data.map(t=>t.id===e.id?s:t),this.emit("keyboard")}}},this.onDragOver=t=>{!this.readOnly&&t.dataTransfer?.types.includes("Files")&&(t.preventDefault(),t.dataTransfer.dropEffect="copy",this.surface?.classList.add("designbase-wc-design-canvas__surface--drop"))},this.onDragLeave=t=>{this.surface?.contains(t.relatedTarget)||this.surface?.classList.remove("designbase-wc-design-canvas__surface--drop")},this.onDrop=t=>{t.preventDefault(),this.surface?.classList.remove("designbase-wc-design-canvas__surface--drop"),!this.readOnly&&t.dataTransfer&&this.importImages(Array.from(t.dataTransfer.files),this.point(t))}}static get observedAttributes(){return["nodes","selected-id","min-zoom","max-zoom","readonly","snap-to-grid","grid-size","show-grid","height","max-file-size","toolbar-mode"]}get nodes(){return clone(this.data)}set nodes(t){try{const e="string"==typeof t?JSON.parse(t):t;if(!Array.isArray(e))return;const i=new Set,s=e.filter(t=>t&&"string"==typeof t.id&&t.id&&!i.has(t.id)&&!!i.add(t.id)).slice(0,100).map(t=>this.normalizeNode(t));if(same(this.data,s))return;this.data.length&&!this.gesture&&this.remember(this.data),this.data=s,this.data.some(t=>t.id===this.selected)||(this.selected=null),this.draw()}catch{}}get selectedId(){return this.selected}set selectedId(t){this.selected=this.data.some(e=>e.id===t)?t:null,this.drawSelection()}get viewport(){return{...this.view}}set viewport(t){this.view={x:num(t?.x,0),y:num(t?.y,0),zoom:clamp(num(t?.zoom,1),this.minZoom,this.maxZoom)},this.drawViewport()}get minZoom(){return clamp(Number(this.getAttribute("min-zoom"))||.1,.05,4)}get maxZoom(){return Math.max(this.minZoom,Math.min(8,Number(this.getAttribute("max-zoom"))||4))}get readOnly(){return this.hasAttribute("readonly")}set readOnly(t){this.toggleAttribute("readonly",t)}connectedCallback(){this.generation++,this.ensureStructure(),this.collectSlots(),this.observer=new MutationObserver(()=>{this.collectSlots(),this.draw()}),this.observer.observe(this,{childList:!0}),this.resizeObserver=new ResizeObserver(()=>this.drawViewport()),this.resizeObserver.observe(this.surface),window.addEventListener("keyup",this.onKeyUp),window.addEventListener("blur",this.onBlur),this.draw()}disconnectedCallback(){this.generation++,this.editing=void 0,this.observer?.disconnect(),this.resizeObserver?.disconnect(),window.removeEventListener("keyup",this.onKeyUp),window.removeEventListener("blur",this.onBlur),this.cancelGesture(),this.space=!1,this.pointers.clear(),this.pinch=void 0;for(const t of this.reading)t.abort();this.reading.clear()}attributeChangedCallback(t){"nodes"===t?this.nodes=this.getAttribute("nodes")||"[]":"selected-id"===t?this.selectedId=this.getAttribute("selected-id"):("readonly"===t&&this.cancelGesture(),this.view.zoom=clamp(this.view.zoom,this.minZoom,this.maxZoom),this.draw())}normalizeNode(e){return{...e,type:["frame","image","text","shape","component"].includes(e.type)?e.type:"frame",name:"string"==typeof e.name?e.name:t("요소"),x:num(e.x,0),y:num(e.y,0),width:clamp(num(e.width,320),16,8e3),height:clamp(num(e.height,240),16,8e3),rotation:num(e.rotation,0),flipX:!0===e.flipX,flipY:!0===e.flipY,opacity:clamp(num(e.opacity,1),0,1),radius:clamp(num(e.radius,0),0,4e3),text:"string"==typeof e.text?e.text.slice(0,1e4):"",fontSize:clamp(num(e.fontSize,24),8,240),fontWeight:clamp(num(e.fontWeight,400),100,900),textColor:e.textColor||"#191919",textAlign:["left","center","right"].includes(e.textAlign||"")?e.textAlign:"left",shape:["rectangle","ellipse","triangle","line"].includes(e.shape||"")?e.shape:"rectangle",component:["button","input","card","badge"].includes(e.component||"")?e.component:"button",stroke:e.stroke||"#191919",strokeWidth:clamp(num(e.strokeWidth,1),0,24)}}ensureStructure(){if(this.surface)return;this.classList.add("designbase-wc-design-canvas"),this.setAttribute("role","region"),this.hasAttribute("aria-label")||this.setAttribute("aria-label",t("디자인 캔버스")),this.toolbar=document.createElement("db-design-toolbar"),this.toolbar.classList.add("designbase-wc-design-canvas__toolbar"),this.toolbar.setAttribute("aria-label",t("캔버스 도구")),this.toolbar.setAttribute("size","s"),this.toolbar.addEventListener("db-tool-action",this.toolAction),this.input=document.createElement("input"),this.input.type="file",this.input.multiple=!0,this.input.accept="image/png,image/jpeg,image/webp,image/gif",this.input.hidden=!0,this.input.addEventListener("change",()=>{this.importImages(Array.from(this.input.files||[])),this.input.value=""}),this.surface=document.createElement("div"),this.surface.className="designbase-wc-design-canvas__surface",this.surface.tabIndex=0,this.surface.setAttribute("aria-label",t("캔버스 작업 영역")),this.surface.setAttribute("aria-describedby",this.id?`${this.id}-help`:""),this.world=document.createElement("div"),this.world.className="designbase-wc-design-canvas__world",this.selection=document.createElement("div"),this.selection.className="designbase-wc-design-canvas__selection";for(const i of["nw","n","ne","e","se","s","sw","w","rotate"]){const s=document.createElement("div");s.dataset.handle=i,s.tabIndex=0,s.setAttribute("role","button"),s.setAttribute("aria-label","rotate"===i?t("선택 요소 회전"):e("선택 요소 크기 조정 {0}",[i])),s.className="designbase-wc-design-canvas__handle",s.title=t("rotate"===i?"회전 · Shift: 15° 단위":"크기 조정 · Shift: 비율 유지"),this.selection.append(s)}const i=document.createElement("span");i.className="designbase-wc-design-canvas__dimensions",this.selection.append(i),this.world.append(this.selection),this.contextBar=document.createElement("db-context-bar"),this.contextBar.classList.add("designbase-wc-design-canvas__context"),this.contextBar.setAttribute("size","s"),this.contextBar.setAttribute("tone","dark"),this.contextBar.setAttribute("placement","top-start"),this.contextBar.addEventListener("db-tool-action",this.toolAction),this.surface.append(this.world,this.contextBar),this.status=document.createElement("div"),this.status.className="designbase-wc-design-canvas__status",this.status.setAttribute("role","status"),this.status.setAttribute("aria-live","polite"),this.status.textContent=t("Space + 드래그 이동 · Ctrl/⌘ + 휠 확대 · 이미지 드롭"),this.surface.addEventListener("pointerdown",this.onPointerDown),this.surface.addEventListener("pointermove",this.onPointerMove),this.surface.addEventListener("pointerup",this.onPointerUp),this.surface.addEventListener("pointercancel",this.onPointerCancel),this.surface.addEventListener("lostpointercapture",this.onLostCapture),this.surface.addEventListener("wheel",this.onWheel,{passive:!1}),this.surface.addEventListener("keydown",this.onKeyDown),this.surface.addEventListener("dblclick",this.beginTextEdit),this.surface.addEventListener("dragover",this.onDragOver),this.surface.addEventListener("dragleave",this.onDragLeave),this.surface.addEventListener("drop",this.onDrop),this.surface.addEventListener("dragstart",t=>t.preventDefault()),this.append(this.toolbar,this.surface,this.status,this.input)}collectSlots(){for(const t of Array.from(this.children))t instanceof HTMLElement&&t.hasAttribute("slot")&&this.slots.set(t.getAttribute("slot"),t)}draw(){if(this.world&&this.surface){if(this.style.height=this.getAttribute("height")||"560px",this.toolbar){const t="floating"===this.getAttribute("toolbar-mode");this.toolbar.toggleAttribute("movable",t),this.toolbar.classList.toggle("designbase-wc-design-canvas__toolbar--floating",t),t&&this.toolbar.parentElement!==this.surface&&this.surface.append(this.toolbar),t||this.toolbar.parentElement===this||this.insertBefore(this.toolbar,this.surface)}this.surface.classList.toggle("designbase-wc-design-canvas__surface--no-grid","false"===this.getAttribute("show-grid"));for(const[t,e]of this.objectElements)if(!this.data.some(e=>e.id===t)){const i=e.querySelector("[slot]");i instanceof HTMLElement&&this.slots.set(i.slot,i),e.remove(),this.objectElements.delete(t)}for(const t of this.data){let e=this.objectElements.get(t.id);if(e||(e=document.createElement("div"),e.className="designbase-wc-design-canvas__node",e.dataset.nodeId=t.id,e.tabIndex=-1,this.world.insertBefore(e,this.selection),this.objectElements.set(t.id,e)),e.style.zIndex=String(this.data.indexOf(t)),e.dataset.nodeType=t.type,e.setAttribute("aria-label",t.name),e.setAttribute("role","group"),Object.assign(e.style,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.width}px`,height:`${t.height}px`,transform:`rotate(${t.rotation||0}deg) scale(${t.flipX?-1:1}, ${t.flipY?-1:1})`,opacity:String(t.opacity??1),borderRadius:`${t.radius||0}px`,background:t.fill||"var(--db-surface-base)"}),"text"===t.type)this.drawText(e,t);else if("shape"===t.type)this.drawShape(e,t);else if("component"===t.type)this.drawComponent(e,t);else if("image"===t.type){let i=e.querySelector("img");i||(i=document.createElement("img"),i.draggable=!1,e.replaceChildren(i)),i.alt=t.name;const s=t.src||"",o=/^(data:image\/(png|jpe?g|webp|gif);base64,|blob:|https?:\/\/|\/[^/])/i.test(s);o&&i.getAttribute("src")!==s&&(i.src=s),o||i.removeAttribute("src")}else if(t.slot&&this.slots.has(t.slot)){const i=this.slots.get(t.slot);i.parentElement!==e&&e.replaceChildren(i)}else t.slot||e.textContent===t.name||(e.textContent=t.name)}this.drawViewport(),this.drawSelection()}}drawText(t,e){let i=t.querySelector(".designbase-wc-design-canvas__text");i||(i=document.createElement("div"),i.className="designbase-wc-design-canvas__text",t.replaceChildren(i)),t.style.background="transparent",t.style.overflow="visible",i.isContentEditable||i.textContent===e.text||(i.textContent=e.text||""),Object.assign(i.style,{fontSize:`${e.fontSize}px`,fontWeight:String(e.fontWeight),color:e.textColor,textAlign:e.textAlign})}drawShape(t,e){t.style.background="transparent";const i="http://www.w3.org/2000/svg",s=e.shape||"rectangle";if(t.dataset.shape!==s||!t.querySelector("svg")){t.dataset.shape=s;const e=document.createElementNS(i,"svg");e.setAttribute("viewBox","0 0 100 100"),e.setAttribute("preserveAspectRatio","none"),e.setAttribute("aria-hidden","true");const o=document.createElementNS(i,"ellipse"===s?"ellipse":"rectangle"===s?"rect":"line"===s?"line":"polygon");e.append(o),t.replaceChildren(e)}const o=t.querySelector("svg").firstElementChild;"rectangle"===s&&(o.setAttribute("x","1"),o.setAttribute("y","1"),o.setAttribute("width","98"),o.setAttribute("height","98"),o.setAttribute("rx",String((e.radius||0)/e.width*100)),o.setAttribute("ry",String((e.radius||0)/e.height*100))),"ellipse"===s&&(o.setAttribute("cx","50"),o.setAttribute("cy","50"),o.setAttribute("rx","49"),o.setAttribute("ry","49")),"triangle"===s&&o.setAttribute("points","50,1 99,99 1,99"),"line"===s&&(o.setAttribute("x1","0"),o.setAttribute("y1","50"),o.setAttribute("x2","100"),o.setAttribute("y2","50")),o.setAttribute("fill","line"===s?"none":e.fill||"#dce8f5"),o.setAttribute("stroke",e.stroke||"#191919"),o.setAttribute("stroke-width",String(e.strokeWidth??1)),o.setAttribute("vector-effect","non-scaling-stroke")}drawComponent(e,i){const s=i.component||"button";let o=e.querySelector(".designbase-wc-design-canvas__component");if(!o||o.dataset.kind!==s){o=document.createElement("div"),o.className="designbase-wc-design-canvas__component",o.inert=!0,o.dataset.kind=s;const i=document.createElement("input"===s?"db-input":"card"===s?"db-card":"badge"===s?"db-badge":"db-button");if("button"===s&&i.setAttribute("variant","primary"),"card"===s){const e=document.createElement("h3");e.dataset.componentText="";const s=document.createElement("p");s.textContent=t("내용을 선택하고 나만의 화면으로 구성하세요."),i.append(e,s)}o.append(i),e.replaceChildren(o)}const n=o.firstElementChild;if("input"===s){const t=n.querySelector("input");t&&Object.assign(t.style,{color:i.textColor,fontSize:`${i.fontSize}px`})}if("input"===s)n.setAttribute("placeholder",i.text||t("내용을 입력하세요"));else if("card"===s){const e=n.querySelector("[data-component-text]");e.textContent=i.text||t("새 카드"),e.style.fontSize=`${i.fontSize}px`}else n.textContent!==i.text&&(n.textContent=i.text||("badge"===s?"Badge":"Button"));Object.assign(n.style,{width:"100%",height:"100%",boxSizing:"border-box",borderRadius:`${i.radius||0}px`,color:i.textColor,fontSize:`${i.fontSize}px`,background:i.fill||"#ffffff"})}drawViewport(){if(!this.world||!this.surface)return;this.world.style.transform=`translate(${this.surface.clientWidth/2+this.view.x}px, ${this.surface.clientHeight/2+this.view.y}px) scale(${this.view.zoom})`,this.style.setProperty("--db-canvas-inverse-zoom",String(1/this.view.zoom));const t=Math.max(4,Number(this.getAttribute("grid-size"))||24)*this.view.zoom;this.surface.style.backgroundSize=`${t}px ${t}px`,this.surface.style.backgroundPosition=`${this.surface.clientWidth/2+this.view.x}px ${this.surface.clientHeight/2+this.view.y}px`,this.surface.classList.toggle("designbase-wc-design-canvas__surface--pan",this.space||this.hand||"pan"===this.gesture?.kind),this.syncToolbar(),this.positionContext()}drawSelection(){if(!this.selection)return;const t=this.data.find(t=>t.id===this.selected);if(this.selection.hidden=!t,this.syncContext(),this.positionContext(),t){Object.assign(this.selection.style,{left:`${t.x}px`,top:`${t.y}px`,width:`${t.width}px`,height:`${t.height}px`,transform:`rotate(${t.rotation||0}deg)`}),this.selection.classList.toggle("designbase-wc-design-canvas__selection--locked",Boolean(t.locked||this.readOnly)),this.selection.querySelector(".designbase-wc-design-canvas__dimensions").textContent=`${Math.round(t.width)} × ${Math.round(t.height)} · ${Math.round(t.rotation||0)}°`;for(const e of Array.from(this.selection.querySelectorAll("[data-handle]")))e.tabIndex=t.locked||this.readOnly?-1:0,e.setAttribute("aria-disabled",String(!!t.locked||this.readOnly))}}syncToolbar(){if(!this.toolbar)return;const e=Math.round(100*this.view.zoom),i=[{id:"select",label:t("선택 도구"),icon:"navigation",shortcut:"V",active:!this.hand},{id:"pan",label:t("화면 이동"),icon:"move",shortcut:"H / Space",active:this.hand},{id:"creation-separator",type:"separator"},{id:"frame",label:t("프레임 추가"),icon:"file-new",disabled:this.readOnly||this.data.length>=100,menuTrigger:"long-press",value:this.creationTool,options:[{id:"frame",label:t("빈 프레임 추가"),icon:"file-new"},{id:"note",label:t("메모 추가"),icon:"documentation"}]},{id:"text",label:t("텍스트 추가"),icon:"text",shortcut:"T",disabled:this.readOnly||this.data.length>=100},{id:"shape",label:t("도형 추가"),icon:"shapes",value:this.shapeTool,disabled:this.readOnly||this.data.length>=100,options:[{id:"rectangle",label:t("사각형"),icon:"shape-rectangle",shortcut:"R"},{id:"ellipse",label:t("타원"),icon:"shape-ellipse",shortcut:"O"},{id:"triangle",label:t("삼각형"),icon:"shape-triangle"},{id:"line",label:t("선"),icon:"shape-line",shortcut:"L"}]},{id:"component",label:t("컴포넌트 추가"),icon:"package",value:this.componentTool,disabled:this.readOnly||this.data.length>=100,options:[{id:"button",label:t("버튼"),icon:"package"},{id:"input",label:t("텍스트 입력"),icon:"documentation"},{id:"card",label:t("카드"),icon:"file-new"},{id:"badge",label:t("배지"),icon:"package"}]},{id:"image",label:t("이미지 추가"),icon:"gallery",disabled:this.readOnly||this.data.length>=100},{id:"history-separator",type:"separator"},{id:"undo",label:t("실행 취소"),icon:"arrow-back-up",shortcut:"⌘/Ctrl+Z",disabled:!this.past.length||this.readOnly},{id:"redo",label:t("다시 실행"),icon:"arrow-back-up",shortcut:"⌘/Ctrl+Shift+Z",disabled:!this.future.length||this.readOnly},{id:"zoom-separator",type:"separator"},{id:"zoom-out",label:t("축소"),icon:"zoom-out",shortcut:"−"},{id:"zoom",label:t("확대율"),text:`${e}%`,value:String(e),options:[...[25,50,100,200,400].filter(t=>t/100>=this.minZoom&&t/100<=this.maxZoom).map(t=>({id:String(t),label:`${t}%`})),{id:"fit",label:t("전체 보기"),icon:"expand",shortcut:"0"}]},{id:"zoom-in",label:t("확대"),icon:"zoom-in",shortcut:"+"},{id:"fit",label:t("전체 보기"),icon:"expand",shortcut:"0"}];this.toolbar.items=i}syncContext(){if(!this.contextBar)return;const e=this.data.find(t=>t.id===this.selected);if(this.contextBar.hidden=!e,!e)return;const i=this.readOnly||!!e.locked;this.contextBar.items=[...e.slot?[]:[{id:"duplicate",label:t("요소 복제"),icon:"copy",disabled:i||this.data.length>=100}],{id:"lock",label:e.locked?t("잠금 해제"):t("요소 잠그기"),icon:e.locked?"unlock":"lock",active:e.locked,disabled:this.readOnly},{id:"transform-separator",type:"separator"},{id:"rotate",label:t("회전"),icon:"refresh",disabled:i,options:[{id:"reset",label:t("회전 초기화")},{id:"left",label:t("왼쪽으로 90° 회전")},{id:"right",label:t("오른쪽으로 90° 회전")}]},{id:"flip-x",label:t("좌우 반전"),icon:"filp-horizontal",active:!!e.flipX,disabled:i},{id:"flip-y",label:t("상하 반전"),icon:"filp-vertical",active:!!e.flipY,disabled:i},{id:"alignment-separator",type:"separator"},...[["left","화면 왼쪽에 정렬","layout-align-left"],["center","가로 중앙 정렬","layout-align-center"],["right","화면 오른쪽에 정렬","layout-align-right"],["top","화면 위쪽에 정렬","layout-align-top"],["middle","세로 중앙 정렬","layout-align-middle"],["bottom","화면 아래쪽에 정렬","layout-align-bottom"]].map(([e,s,o])=>({id:`align-${e}`,label:t(s),icon:o,disabled:i})),{id:"layer-separator",type:"separator"},{id:"arrange",label:t("레이어 순서"),icon:"layer",disabled:i,options:[{id:"front",label:t("맨 앞으로 가져오기")},{id:"back",label:t("맨 뒤로 보내기")}]},{id:"delete",label:t("요소 삭제"),icon:"trash",shortcut:"Delete",disabled:i}]}insertNode(e,i){if(this.readOnly||this.data.length>=100)return;this.remember(this.data),"frame"===e&&(this.creationTool=i||this.creationTool),"shape"===e&&(this.shapeTool=i||this.shapeTool),"component"===e&&(this.componentTool=i||this.componentTool);const s="frame"===e&&"note"===this.creationTool;let o=320,n=240,a=t(s?"새로운 메모":"새 프레임"),r="";if(s&&(o=220,n=180),"text"===e&&(o=280,n=100,a=t("텍스트"),r=t("새로운 아이디어")),"shape"===e&&(o=200,n="line"===this.shapeTool?24:160,a=t({rectangle:"사각형",ellipse:"타원",triangle:"삼각형",line:"선"}[this.shapeTool])),"component"===e){const t={button:[160,44,"Button"],input:[240,44,"내용을 입력하세요"],card:[280,180,"새 카드"],badge:[100,32,"Badge"]};[o,n,r]=t[this.componentTool]||t.button,a=`${r} 컴포넌트`}const h=this.normalizeNode({id:crypto.randomUUID(),type:e,name:a,x:-this.view.x/this.view.zoom-o/2,y:-this.view.y/this.view.zoom-n/2,width:o,height:n,radius:"component"===e?10:s?12:0,fill:"shape"===e?"#dce8f5":"component"===e&&"button"===this.componentTool?"#191919":s?"#edf2e9":"#ffffff",text:r,fontSize:"component"===e?"card"===this.componentTool?22:14:24,textColor:"component"===e&&"button"===this.componentTool?"#ffffff":"#191919",shape:this.shapeTool,component:this.componentTool});this.data=[...this.data,h],this.select(h.id),this.emit("create")}positionContext(){const t=this.data.find(t=>t.id===this.selected);if(!t||!this.surface||!this.contextBar)return;const e=this.contextBar.offsetWidth||200,i=this.contextBar.offsetHeight||40,s=(t.rotation||0)*Math.PI/180,o=(Math.abs(t.width*Math.sin(s))+Math.abs(t.height*Math.cos(s)))/2*this.view.zoom,n=this.surface.clientWidth/2+this.view.x+(t.x+t.width/2)*this.view.zoom,a=this.surface.clientHeight/2+this.view.y+(t.y+t.height/2)*this.view.zoom,r=a-o-i-44,h=a+o+36,d=r>=12?r:h+i<=this.surface.clientHeight-12?h:12;this.contextBar.style.left=`${clamp(n,e/2+12,Math.max(e/2+12,this.surface.clientWidth-e/2-12))}px`,this.contextBar.style.top=`${clamp(d,12,Math.max(12,this.surface.clientHeight-i-12))}px`}select(t){t!==this.selected&&(this.selected=t,this.drawSelection(),this.dispatchEvent(new CustomEvent("db-selection-change",{detail:{selectedId:t,node:this.data.find(e=>e.id===t)||null},bubbles:!0,composed:!0})))}emit(t){this.draw(),this.dispatchEvent(new CustomEvent("db-nodes-change",{detail:{nodes:this.nodes,selectedId:this.selected,reason:t},bubbles:!0,composed:!0}))}emitViewport(){this.drawViewport(),this.dispatchEvent(new CustomEvent("db-viewport-change",{detail:this.viewport,bubbles:!0,composed:!0}))}remember(t){this.past.push(clone(t)),this.past.length>40&&this.past.shift(),this.future=[]}undo(){!this.readOnly&&this.past.length&&(this.cancelGesture(),this.future.push(this.nodes),this.data=this.past.pop(),this.select(this.data.some(t=>t.id===this.selected)?this.selected:this.data[0]?.id||null),this.emit("undo"))}redo(){!this.readOnly&&this.future.length&&(this.cancelGesture(),this.past.push(this.nodes),this.data=this.future.pop(),this.select(this.data.some(t=>t.id===this.selected)?this.selected:this.data[0]?.id||null),this.emit("redo"))}point(t){const e=this.surface.getBoundingClientRect();return{x:(t.clientX-e.left-this.surface.clientWidth/2-this.view.x)/this.view.zoom,y:(t.clientY-e.top-this.surface.clientHeight/2-this.view.y)/this.view.zoom}}zoomTo(t,e){if(!this.surface)return;const i=this.surface.getBoundingClientRect(),s=e||{x:i.left+i.width/2,y:i.top+i.height/2},o=this.point({clientX:s.x,clientY:s.y});this.view.zoom=clamp(t,this.minZoom,this.maxZoom),this.view.x=s.x-i.left-this.surface.clientWidth/2-o.x*this.view.zoom,this.view.y=s.y-i.top-this.surface.clientHeight/2-o.y*this.view.zoom,this.emitViewport(),this.drawSelection()}fitToContent(){if(!this.surface)return;if(!this.data.length)return this.viewport={x:0,y:0,zoom:1},void this.emitViewport();const t=this.data.flatMap(t=>{const e=(t.rotation||0)*Math.PI/180,i=t.x+t.width/2,s=t.y+t.height/2;return[[-1,-1],[1,-1],[1,1],[-1,1]].map(([o,n])=>({x:i+o*t.width/2*Math.cos(e)-n*t.height/2*Math.sin(e),y:s+o*t.width/2*Math.sin(e)+n*t.height/2*Math.cos(e)}))}),e=Math.min(...t.map(t=>t.x)),i=Math.max(...t.map(t=>t.x)),s=Math.min(...t.map(t=>t.y)),o=Math.max(...t.map(t=>t.y)),n=clamp(Math.min((this.surface.clientWidth-96)/Math.max(16,i-e),(this.surface.clientHeight-96)/Math.max(16,o-s)),this.minZoom,Math.min(1,this.maxZoom));this.view={zoom:n,x:-(e+i)/2*n,y:-(s+o)/2*n},this.emitViewport(),this.drawSelection()}editable(t){return t.composedPath().some(t=>t instanceof HTMLElement&&(t.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))}cancelGesture(){const t=this.gesture;this.gesture=void 0,t&&"pan"!==t.kind&&(this.data=t.before,this.isConnected&&this.emit("cancel")),t&&this.surface?.hasPointerCapture(t.pointer)&&this.surface.releasePointerCapture(t.pointer)}async importImages(t,i){if(this.readOnly)return;const s=i||{x:-this.view.x/this.view.zoom,y:-this.view.y/this.view.zoom},o=this.generation,n=[];let a=0;const r=Math.max(1,Number(this.getAttribute("max-file-size"))||10485760);for(const e of t.slice(0,12))if(!["image/png","image/jpeg","image/webp","image/gif"].includes(e.type)||e.size>r||this.data.length+n.length>=100)a++;else try{const t=await new Promise((t,i)=>{const s=new FileReader;this.reading.add(s),s.onload=()=>{this.reading.delete(s),t(String(s.result))},s.onerror=s.onabort=()=>{this.reading.delete(s),i(new Error("이미지를 읽지 못했습니다."))},s.readAsDataURL(e)}),i=await new Promise((e,i)=>{const s=new Image;s.onload=()=>e({width:s.naturalWidth,height:s.naturalHeight}),s.onerror=i,s.src=t});if(!this.isConnected||o!==this.generation)return;const a=Math.min(1,480/Math.max(i.width,i.height));n.push({id:crypto.randomUUID(),type:"image",name:e.name,src:t,x:s.x+24*n.length-i.width*a/2,y:s.y+24*n.length-i.height*a/2,width:Math.max(16,i.width*a),height:Math.max(16,i.height*a),rotation:0,opacity:1,radius:0})}catch{a++}this.isConnected&&o===this.generation&&(n.length&&(this.remember(this.data),this.data=[...this.data,...n.map(t=>this.normalizeNode(t))],this.select(n[n.length-1].id),this.emit("import")),this.status&&(this.status.textContent=e("{0}개 이미지 추가{1}",[n.length,a||t.length>12?` · ${a+Math.max(0,t.length-12)}개 제외 (PNG/JPG/WebP/GIF, 파일당 ${Math.round(r/1024/1024)}MB 이하)`:""])))}}customElements.get("db-design-canvas")||customElements.define("db-design-canvas",DbDesignCanvas);export{DbDesignCanvas};
|
|
@@ -4,7 +4,7 @@ export { VideoClipEffect, VideoEffectProperty } from './media-editor-model.js';
|
|
|
4
4
|
|
|
5
5
|
interface TimelineEditDetail {
|
|
6
6
|
clips: VideoClip[];
|
|
7
|
-
action: 'undo' | 'redo' | 'keyframe' | 'move' | 'trim-start' | 'trim-end' | 'fade-in' | 'fade-out' | 'delete' | 'split';
|
|
7
|
+
action: 'undo' | 'redo' | 'effect-range' | 'keyframe' | 'move' | 'trim-start' | 'trim-end' | 'fade-in' | 'fade-out' | 'delete' | 'split';
|
|
8
8
|
clipId: string;
|
|
9
9
|
effectId?: string;
|
|
10
10
|
propertyId?: string;
|
|
@@ -37,6 +37,17 @@ declare class DbVideoTimeline extends HTMLElement {
|
|
|
37
37
|
private resize?;
|
|
38
38
|
private resizeDrag?;
|
|
39
39
|
private paneGrip?;
|
|
40
|
+
private rangeDrag?;
|
|
41
|
+
get layerGap(): number;
|
|
42
|
+
set layerGap(value: number);
|
|
43
|
+
private isSource;
|
|
44
|
+
private rangeFinish;
|
|
45
|
+
private renderRange;
|
|
46
|
+
private labelDrag?;
|
|
47
|
+
get labelWidth(): number;
|
|
48
|
+
set labelWidth(value: number);
|
|
49
|
+
private get contentDuration();
|
|
50
|
+
private labelGrip;
|
|
40
51
|
get rowHeight(): number;
|
|
41
52
|
set rowHeight(n: number);
|
|
42
53
|
get height(): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./db-button.js";import{DbContextMenu as t}from"./db-context-menu.js";import{DbKeyframeEditor as e}from"./db-keyframe-editor.js";import{t as i,subscribeDbLocale as s}from"../core/i18n.js";import{frameTime as r,emitEditor as a,clamp as d,editorEditable as n,finite as o,editorButton as c,evaluateKeyframes as h,editorTimecode as l,applyKeyframeEasing as f}from"./media-editor-model.js";class DbVideoTimeline extends HTMLElement{constructor(){super(...arguments),this.effectTreeId=`effects-${crypto.randomUUID()}`,this.effectKeySelection=new Map,this.expandedEffects=new Set,this.activeProperty=new Map,this.effectEditing=!1,this.undoClips=[],this.redoClips=[],this.committedClips=[],this.selectedEffectKeys=new Set,this._tracks=[],this._clips=[],this._markers=[],this.raf=0,this.down=t=>{if(this.drag||this.resizeDrag)return;if(this.disabled||0!==t.button||!(t.target instanceof Element)||t.target.closest("db-button,db-keyframe-editor,[data-effect-clip]"))return;const e=t.target.closest("[data-track-resize]"),i=t.target.closest("[data-pane-resize]");if(e||i){t.preventDefault(),this.viewport?.focus({preventScroll:!0});const i=this._tracks.find(t=>t.id===e?.dataset.trackResize);return this.resizeDrag={id:t.pointerId,trackId:i?.id,startY:t.clientY,initial:i?this.trackHeight(i):this.viewport.getBoundingClientRect().height,beforeTracks:this.tracks,beforeHeight:this.height},void this.setPointerCapture(t.pointerId)}const s=t.target.closest("[data-marker]");if(s)return void this.seek(Number(s.dataset.marker));const r=t.target.closest("[data-clip]"),a=this._clips.find(t=>t.id===r?.dataset.clip);if(a){if(this.selectedEffectKeys.clear(),this.select(a.id),this._tracks.find(t=>t.id===a.trackId)?.locked)return;this.viewport?.focus({preventScroll:!0})}else if(!t.target.closest("[data-seek],[data-playhead]"))return;if("touch"===t.pointerType&&!a&&!t.target.closest(".db-vt__ruler,[data-playhead]"))return;t.preventDefault();const d=t.target.closest("[data-trim]")?.dataset.trim,n=t.target.closest("[data-fade]")?.dataset.fade;this.drag={kind:a?"in"===n?"fade-in":"out"===n?"fade-out":"start"===d?"trim-start":"end"===d?"trim-end":"move":"seek",id:t.pointerId,clipId:a?.id,originX:t.clientX,originY:t.clientY,scroll:this.viewport.scrollLeft,before:this.clips,x:t.clientX,y:t.clientY,changed:!1,shift:t.shiftKey,ripple:t.metaKey||t.ctrlKey,duplicate:a&&!d&&!n&&t.altKey?`clip-${crypto.randomUUID()}`:void 0},this.setPointerCapture(t.pointerId),a||this.seek(this.timeAt(t.clientX)),this.raf=requestAnimationFrame(this.autoScroll)},this.move=t=>{if(this.marquee?.pointer===t.pointerId){const e=this.marquee,i=this.getBoundingClientRect(),s=Math.min(e.x,t.clientX),r=Math.min(e.y,t.clientY),a=Math.max(e.x,t.clientX),d=Math.max(e.y,t.clientY);Object.assign(e.box.style,{left:s-i.left+"px",top:r-i.top+"px",width:a-s+"px",height:d-r+"px"}),this.selectedEffectKeys=new Set(e.before);for(const t of this.querySelectorAll("[data-key-token]")){if(t.closest("[data-effect-clip]")?.dataset.effectClip!==e.clipId||t.hasAttribute("disabled"))continue;const i=t.getBoundingClientRect(),n=i.x+i.width/2,o=i.y+i.height/2;n>=s&&n<=a&&o>=r&&o<=d&&this.selectedEffectKeys.add(t.dataset.keyToken)}return void this.syncEffectSelection()}if(this.effectDrag?.pointer===t.pointerId){const e=this.effectDrag,i=e.before.find(t=>t.id===e.clipId);if(t.altKey&&!e.copies&&(e.copies=this.makeKeyCopyIds()),e.copies){const s=r((t.clientX-e.x)/this.scale,this.fps);if(0===s){this._clips=e.before,e.changed=!1;for(const t of this.querySelectorAll("[data-duplicate-preview]"))t.remove();return void this.syncEffectSelection()}const d=this.copyKeyData(e.before,e.clipId,s,e.copies);if(!d)return;this._clips=d,e.changed=!0;for(const[t,s]of e.copies){const[e,r,a]=JSON.parse(t),n=this.effectToken(e,r,a,s),o=Array.from(this.querySelectorAll("[data-key-token]")).find(e=>e.dataset.keyToken===t);if(!o)continue;let c=Array.from(this.querySelectorAll("[data-key-token]")).find(t=>t.dataset.keyToken===n);c||(c=o.cloneNode(!0),c.dataset.keyToken=n,c.dataset.propertyKey=s,c.dataset.duplicatePreview="true",o.parentElement.append(c));const h=d.find(t=>t.id===e)?.effects?.find(t=>t.id===r)?.properties?.find(t=>t.id===a)?.keyframes.find(t=>t.id===s);h&&(c.style.left=(i.start+h.time)*this.scale+"px"),c.setAttribute("aria-pressed","true"),o.setAttribute("aria-pressed","false")}return void a(this,"db-input",{clips:this.clips,clipId:e.clipId,action:"keyframe"})}let s=-1/0,n=1/0;for(const t of i.effects||[])for(const e of t.properties||[]){const r=[...e.keyframes].sort((t,e)=>t.time-e.time);r.forEach((a,d)=>{if(!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,a.id)))return;const o=r.slice(0,d).reverse().find(s=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,s.id))),c=r.slice(d+1).find(s=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,s.id)));s=Math.max(s,(o?o.time+1/this.fps:0)-a.time),n=Math.min(n,(c?c.time-1/this.fps:i.duration)-a.time)})}const o=r(d((t.clientX-e.x)/this.scale,s,n),this.fps);this._clips=e.before.map(t=>t.id!==e.clipId?t:{...t,effects:t.effects?.map(e=>({...e,properties:e.properties?.map(i=>({...i,keyframes:i.keyframes.map(s=>this.selectedEffectKeys.has(this.effectToken(t.id,e.id,i.id,s.id))?{...s,time:r(s.time+o,this.fps)}:s)}))}))}),e.changed=0!==o;for(const t of this.querySelectorAll("[data-key-token]")){if(!this.selectedEffectKeys.has(t.dataset.keyToken))continue;const[e,i,s,r]=JSON.parse(t.dataset.keyToken),a=this._clips.find(t=>t.id===e),d=a?.effects?.find(t=>t.id===i)?.properties?.find(t=>t.id===s)?.keyframes.find(t=>t.id===r);a&&d&&(t.style.left=(a.start+d.time)*this.scale+"px")}return void a(this,"db-input",{clips:this.clips,clipId:e.clipId,action:"keyframe",effectId:e.effectId,propertyId:e.propertyId})}if(this.resizeDrag?.id===t.pointerId){const e=this.resizeDrag,i=Math.round(e.initial+t.clientY-e.startY);return void(e.trackId?(this._tracks=e.beforeTracks.map(t=>t.id===e.trackId?{...t,height:d(i,32,120)}:t),this.render(),a(this,"db-track-input",{tracks:this.tracks})):(this.height=d(i,120,720),a(this,"db-resize-input",{height:this.height})))}this.drag?.id===t.pointerId&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.drag.shift=t.shiftKey,this.drag.ripple=t.metaKey||t.ctrlKey,"move"===this.drag.kind&&t.altKey&&!this.drag.duplicate&&(this.drag.duplicate=`clip-${crypto.randomUUID()}`),this.updateDrag())},this.autoScroll=()=>{if(!this.drag||!this.viewport)return;const t=this.viewport.getBoundingClientRect(),e=this.drag.x,i=e>t.right-48?d((e-t.right+48)/3,0,18):e<t.left+144+32?-d((t.left+176-e)/3,0,18):0,s=this.viewport.scrollLeft;this.viewport.scrollLeft+=i,s!==this.viewport.scrollLeft&&this.updateDrag(),this.raf=requestAnimationFrame(this.autoScroll)},this.recordTimelineHistory=t=>{if(t.target!==this)return;const e=t.detail;e?.clips&&"undo"!==e.action&&"redo"!==e.action&&JSON.stringify(e.clips)!==JSON.stringify(this.committedClips)&&(this.undoClips.push(structuredClone(this.committedClips)),this.undoClips.length>100&&this.undoClips.shift(),this.redoClips=[],this.committedClips=structuredClone(e.clips))},this.effectContextMenu=e=>{const s=e.target,r=s.closest("[data-effect-clip]");if(!r)return;const a=this._clips.find(t=>t.id===r.dataset.effectClip);if(!a)return;e.preventDefault(),e.stopPropagation(),this.viewport?.focus({preventScroll:!0});const d=s.closest("[data-key-token]");d&&!this.selectedEffectKeys.has(d.dataset.keyToken)&&(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(d.dataset.keyToken),this.syncEffectSelection());const n=this.disabled||!!this._tracks.find(t=>t.id===a.trackId)?.locked||!this.selectedEffectKeys.size||!!d?.hasAttribute("disabled");this.keyMenu||(this.keyMenu=new t,this.append(this.keyMenu)),this.keyMenu.items=[{id:"duplicate",label:i("media.duplicateKeys"),icon:"copy",disabled:n},{id:"easing",label:i("media.keyEasing"),disabled:n,subItems:["linear","hold","ease-in","ease-out","ease-in-out"].map(t=>({id:`ease:${t}`,label:i("media."+t)}))},{id:"divider",divider:!0},{id:"delete",label:i("media.deleteKey"),icon:"trash",variant:"danger",disabled:n},{id:"undo",label:i("media.undo"),disabled:!this.undoClips.length}],this.keyMenu.oncontextmenu=t=>t.preventDefault(),this.keyMenu.addEventListener("db-select",this.keyMenuSelect),this.keyMenu.dataset.clipId=a.id,this.keyMenu.openAt(e.clientX,e.clientY)},this.keyMenuSelect=t=>{t.stopPropagation();const e=t.detail.item.id,i=this.keyMenu.dataset.clipId;"duplicate"===e?this.duplicateSelectedKeys(i):"delete"===e?this.editSelectedKeys(i):e.startsWith("ease:")?this.editSelectedKeys(i,e.slice(5)):"undo"===e&&this.restoreTimelineHistory()},this.up=t=>{this.marquee?.pointer!==t.pointerId?this.effectDrag?.pointer!==t.pointerId?this.resizeDrag?.id!==t.pointerId?this.drag?.id===t.pointerId&&(this.drag.changed&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.updateDrag()),this.finish()):this.finishResize():this.finishEffectDrag():this.finishMarquee()},this.cancel=t=>{[this.drag?.id,this.resizeDrag?.id,this.effectDrag?.pointer,this.marquee?.pointer].includes(t.pointerId)&&("lostpointercapture"!==t.type||t.target===this&&!this.hasPointerCapture(t.pointerId))&&(this.finishMarquee(!0),this.finishEffectDrag(!0),this.finish(!0),this.finishResize(!0))},this.onActionClick=t=>{const e=t.target,i=e.dataset.action;if(!this.disabled&&("scroll"===i&&this.viewport?.scrollBy({left:Math.max(160,this.viewport.clientWidth-144),behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"instant":"smooth"}),"lock"===i||"mute"===i)){const t=e.dataset.track;this._tracks=this._tracks.map(e=>e.id===t?{...e,["lock"===i?"locked":"muted"]:!e["lock"===i?"locked":"muted"]}:e),this.render(),a(this,"db-track-change",{tracks:this.tracks})}},this.doubleResize=t=>{if(this.disabled||!(t.target instanceof Element))return;const e=t.target.closest("[data-track-resize],[data-pane-resize]");e&&(e.dataset.trackResize?(this._tracks=this._tracks.map(t=>t.id===e.dataset.trackResize?{...t,height:this.rowHeight}:t),this.render(),a(this,"db-track-change",{tracks:this.tracks})):(this.height=0,a(this,"db-resize",{height:0})))},this.wheel=t=>{if(this.disabled||!this.shortcuts||this.drag||this.resizeDrag||!t.metaKey&&!t.ctrlKey)return;t.preventDefault(),t.stopPropagation();const e=Math.exp(-t.deltaY*(1===t.deltaMode?.04:.002));this.changeZoom(this.zoom*e,t.clientX-this.viewport.getBoundingClientRect().left,"wheel")},this.key=t=>{if(!this.disabled&&!n(t.target)&&(t.metaKey||t.ctrlKey)&&"z"===t.key.toLowerCase()&&!this.drag&&!this.effectDrag&&!this.effectEditing)return void((t.shiftKey?this.redoClips:this.undoClips).length&&(t.preventDefault(),t.stopPropagation(),this.restoreTimelineHistory(t.shiftKey)));if(!(this.disabled||n(t.target)||t.metaKey||t.ctrlKey||t.altKey||"u"!==t.key.toLowerCase())){const e=this._clips.find(t=>t.id===this.selectedClipId&&t.effects?.some(t=>t.properties?.length))||this._clips.find(t=>t.effects?.some(t=>t.properties?.length));if(e){t.preventDefault(),t.stopPropagation();const i=!this.expandedEffects.has(e.id);for(const t of[e.id,...(e.effects||[]).map(t=>`${e.id}/${t.id}`)])i?this.expandedEffects.add(t):this.expandedEffects.delete(t);this.render(),this.viewport?.focus({preventScroll:!0})}return}if("Escape"===t.key&&this.marquee)return t.preventDefault(),void this.finishMarquee(!0);if(!this.disabled&&!n(t.target)&&!t.target?.closest("db-keyframe-editor")&&["Delete","Backspace"].includes(t.key)&&this.selectedEffectKeys.size){t.preventDefault();const e=new Set(Array.from(this.selectedEffectKeys).map(t=>JSON.parse(t)[0]));for(const t of e)this.editSelectedKeys(t);return}if(this.disabled||n(t.target)||t.isComposing||t.target?.closest("db-keyframe-editor"))return;if("Escape"===t.key&&this.effectDrag)return t.preventDefault(),void this.finishEffectDrag(!0);if("Escape"===t.key&&this.drag)return t.preventDefault(),void this.finish(!0);if("Escape"===t.key&&this.resizeDrag)return t.preventDefault(),void this.finishResize(!0);const e=t.target.closest("[data-track-resize],[data-pane-resize]");if(e&&["ArrowUp","ArrowDown","Home"].includes(t.key)){t.preventDefault();const i="ArrowUp"===t.key?-1:1;return void(e.dataset.trackResize?(this._tracks=this._tracks.map(s=>s.id===e.dataset.trackResize?{...s,height:"Home"===t.key?this.rowHeight:d(this.trackHeight(s)+i*(t.shiftKey?8:2),32,120)}:s),this.render(),a(this,"db-track-change",{tracks:this.tracks}),this.querySelector(`[data-track-resize="${CSS.escape(e.dataset.trackResize)}"]`)?.focus()):(this.height="Home"===t.key?0:d((this.height||this.viewport.clientHeight)+i*(t.shiftKey?24:8),120,720),a(this,"db-resize",{height:this.height}),this.paneGrip?.focus()))}if(this.shortcuts&&!t.altKey&&["+","=","-","0"].includes(t.key))return t.preventDefault(),t.stopPropagation(),void("0"===t.key?this.changeZoom((this.viewport.clientWidth-144)/this.duration/.8,void 0,"fit"):this.changeZoom(this.zoom*("-"===t.key?1/1.2:1.2)));if(this.disabled||n(t.target)||t.ctrlKey||t.metaKey||t.altKey)return;const i=t.target.closest("[data-trim],[data-fade]");if(i&&["ArrowLeft","ArrowRight"].includes(t.key)){const e=i.closest("[data-clip]"),s=this._clips.find(t=>t.id===e?.dataset.clip);if(!s||this._tracks.find(t=>t.id===s.trackId)?.locked)return;t.preventDefault();const r="start"===i.dataset.trim?"trim-start":"end"===i.dataset.trim?"trim-end":"in"===i.dataset.fade?"fade-in":"fade-out";this.drag={kind:r,id:-1,clipId:s.id,originX:0,originY:0,scroll:this.viewport.scrollLeft,before:this.clips,x:("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps*this.scale,y:0,changed:!1,shift:!0,ripple:t.metaKey||t.ctrlKey},this.updateDrag(),this.finish();const a=i.dataset.focus;return void Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===a)?.focus({preventScroll:!0})}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key))t.preventDefault(),this.seek("Home"===t.key?0:"End"===t.key?this.duration:this.currentTime+("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps),this.scrollToTime(this.currentTime);else if("s"===t.key.toLowerCase())t.preventDefault(),this.splitSelected();else if("Delete"===t.key||"Backspace"===t.key){const e=this._clips.find(t=>t.id===this.selectedClipId);if(!e||this._tracks.find(t=>t.id===e.trackId)?.locked)return;t.preventDefault(),this._clips=this._clips.filter(t=>t.id!==e.id),this.select(""),this.render(),a(this,"db-change",{clips:this.clips,clipId:e.id,action:"delete"})}else"Enter"===t.key&&t.target instanceof HTMLElement&&t.target.dataset.clip&&(t.preventDefault(),this.select(t.target.dataset.clip))}}static get observedAttributes(){return["duration","fps","current-time","zoom","snap","disabled","selected-clip-id","row-height","height","min-clip-duration","shortcuts"]}get rowHeight(){return d(this.number("row-height",42),32,120)}set rowHeight(t){this.setAttribute("row-height",String(t))}get height(){const t=this.number("height",0);return t>0?d(t,120,720):0}set height(t){t>0?this.setAttribute("height",String(t)):this.removeAttribute("height")}get minClipDuration(){return Math.min(this.duration,Math.max(1/this.fps,Math.ceil(this.number("min-clip-duration",.5)*this.fps)/this.fps))}set minClipDuration(t){this.setAttribute("min-clip-duration",String(t))}get shortcuts(){return"false"!==this.getAttribute("shortcuts")}set shortcuts(t){this.setAttribute("shortcuts",String(t))}trackHeight(t){return d(o(t.height,this.rowHeight),32,120)}number(t,e){const i=this.getAttribute(t);return i?.trim()&&Number.isFinite(Number(i))?Number(i):e}get duration(){return d(this.number("duration",30),1/this.fps,86400)}set duration(t){this.setAttribute("duration",String(t))}get fps(){return d(Math.round(this.number("fps",30)),1,120)}set fps(t){this.setAttribute("fps",String(t))}get zoom(){return d(this.number("zoom",100),10,800)}set zoom(t){this.setAttribute("zoom",String(t))}get currentTime(){return r(d(this.number("current-time",0),0,this.duration),this.fps)}set currentTime(t){this.setAttribute("current-time",String(t))}get snap(){return"false"!==this.getAttribute("snap")}set snap(t){this.setAttribute("snap",String(t))}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",t)}get selectedClipId(){return this.getAttribute("selected-clip-id")||""}set selectedClipId(t){this.setAttribute("selected-clip-id",t)}get tracks(){return this._tracks.map(t=>({...t}))}set tracks(t){const e=new Set;this._tracks=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>({...t,label:String(t.label||t.id),kind:["video","audio","title","mixed"].includes(t.kind)?t.kind:"video"})),this.render()}get clips(){return this._clips.map(t=>({...t,...t.effects?{effects:t.effects.map(t=>structuredClone(t))}:{}}))}set clips(t){if(this.drag||this.effectEditing)return;const e=new Set;this._clips=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>{const e=r(d(o(t.start,0),0,this.duration-1/this.fps),this.fps),i=Math.min(this.duration-e,Math.max(1/this.fps,r(o(t.duration,1),this.fps))),s=r(d(o(t.fadeIn,0),0,i),this.fps),a=r(d(o(t.fadeOut,0),0,Math.max(0,i-s)),this.fps);return{...t,label:String(t.label||t.id),start:e,duration:i,sourceOffset:Math.max(0,o(t.sourceOffset,0)),...void 0!==t.fadeIn?{fadeIn:s}:{},...void 0!==t.fadeOut?{fadeOut:a}:{},...t.effects?{effects:Array.isArray(t.effects)?t.effects.filter(t=>t&&"string"==typeof t.id).map(t=>({...t,label:String(t.label||t.id)})):[]}:{}}}),JSON.stringify(this._clips)!==JSON.stringify(this.committedClips)&&(this.committedClips=this.clips,this.undoClips=[],this.redoClips=[]),this.render()}get markers(){return this._markers.map(t=>({...t}))}set markers(t){this._markers=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&Number.isFinite(t.time)).map(t=>({...t,label:String(t.label),time:d(t.time,0,this.duration)})),this.render()}get scale(){return.8*this.zoom}connectedCallback(){if(this.classList.add("designbase-wc-video-timeline"),this.setAttribute("role","region"),!this.viewport){this.viewport=document.createElement("div"),this.viewport.className="db-vt__viewport",this.viewport.tabIndex=0,this.canvas=document.createElement("div"),this.canvas.className="db-vt__canvas",this.viewport.append(this.canvas),this.hint=document.createElement("div"),this.hint.className="db-vt__hint";const t=c(i("media.more"),"chevron-right","scroll");this.hint.append(t),this.paneGrip=document.createElement("div"),this.paneGrip.className="db-vt__pane-resize",this.paneGrip.dataset.paneResize="",this.paneGrip.tabIndex=0,this.paneGrip.setAttribute("role","separator"),this.paneGrip.setAttribute("aria-orientation","horizontal"),this.append(this.viewport,this.hint,this.paneGrip),this.viewport.addEventListener("scroll",()=>this.syncHint())}this.viewport.addEventListener("wheel",this.wheel,{passive:!1}),this.addEventListener("dblclick",this.doubleResize),this.addEventListener("db-change",this.recordTimelineHistory,!0),this.addEventListener("contextmenu",this.effectContextMenu),this.addEventListener("pointerdown",this.down),this.addEventListener("pointermove",this.move),this.addEventListener("pointerup",this.up),this.addEventListener("pointercancel",this.cancel),this.addEventListener("lostpointercapture",this.cancel),this.addEventListener("keydown",this.key),this.addEventListener("db-click",this.onActionClick),this.unsubscribe=s(()=>this.render()),this.resize=new ResizeObserver(()=>this.syncHint()),this.resize.observe(this),this.render()}disconnectedCallback(){this.rollback(),this.resize?.disconnect(),this.unsubscribe?.(),this.viewport?.removeEventListener("wheel",this.wheel),this.removeEventListener("dblclick",this.doubleResize),this.finishResize(!0),this.removeEventListener("db-change",this.recordTimelineHistory,!0),this.removeEventListener("contextmenu",this.effectContextMenu),this.keyMenu?.remove(),this.keyMenu=void 0,this.removeEventListener("pointerdown",this.down),this.removeEventListener("pointermove",this.move),this.removeEventListener("pointerup",this.up),this.removeEventListener("pointercancel",this.cancel),this.removeEventListener("lostpointercapture",this.cancel),this.removeEventListener("keydown",this.key),this.removeEventListener("db-click",this.onActionClick)}attributeChangedCallback(t){"disabled"===t&&this.disabled&&(this.finishEffectDrag(!0),this.finish(!0),this.finishResize(!0)),"current-time"===t?this.syncCursor():this.render()}syncHint(){for(const t of this.querySelectorAll(".db-vt__selection-tools"))t.style.width=`${this.viewport?.clientWidth||240}px`;if(this.syncCursor(),this.viewport)for(const t of this.querySelectorAll("[data-marker]"))t.style.visibility=Number(t.dataset.marker)*this.scale<this.viewport.scrollLeft?"hidden":"";this.viewport&&this.hint&&(this.hint.hidden=this.viewport.scrollLeft+this.viewport.clientWidth>=this.viewport.scrollWidth-2)}syncCursor(){for(const t of this.querySelectorAll("[data-effect-property]")){const e=this._clips.find(e=>e.id===t.closest("[data-effect-clip]")?.dataset.effectClip),i=e?.effects?.find(e=>e.id===t.dataset.effectId)?.properties?.find(e=>e.id===t.dataset.effectProperty),s=t.querySelector(".db-vt__property-value");e&&i&&s&&(s.textContent=String(Math.round(100*h(i.keyframes,this.currentTime-e.start))/100))}if(!this.effectEditing)for(const t of this.querySelectorAll("db-keyframe-editor[data-effect-editor]")){const e=this._clips.find(e=>e.id===t.closest("[data-effect-clip]")?.dataset.effectClip);e&&(t.currentTime=d(this.currentTime-e.start,0,e.duration))}this.cursor&&(this.cursor.style.left=144+this.currentTime*this.scale+"px",this.cursor.style.visibility=this.currentTime*this.scale<(this.viewport?.scrollLeft||0)?"hidden":"",this.cursor.setAttribute("aria-valuenow",String(this.currentTime)),this.cursor.setAttribute("aria-valuetext",l(this.currentTime,this.fps)))}seek(t){this.currentTime=r(d(t,0,this.duration),this.fps),a(this,"db-seek",{time:this.currentTime})}scrollToTime(t){this.viewport&&(this.viewport.scrollLeft=Math.max(0,d(t,0,this.duration)*this.scale-(this.viewport.clientWidth-144)/2))}setZoom(t){const e=this.currentTime;this.zoom=t,this.scrollToTime(e)}node(t,e,i){const s=document.createElement(t);return s.className=e,void 0!==i&&(s.textContent=i),s}render(){if(this.effectEditing)return;const t=new Set(this._clips.flatMap(t=>(t.effects||[]).flatMap(e=>(e.properties||[]).flatMap(i=>i.keyframes.map(s=>this.effectToken(t.id,e.id,i.id,s.id))))));if(this.selectedEffectKeys=new Set(Array.from(this.selectedEffectKeys).filter(e=>t.has(e))),!this.canvas||!this.viewport)return;this.querySelector(":scope > .db-vt__readout")?.remove(),this.viewport.style.height=this.height?`${this.height}px`:"",this.viewport.style.maxHeight=this.height?"720px":"360px",this.paneGrip&&(this.paneGrip.setAttribute("aria-label",i("media.resizeTimeline")),this.paneGrip.setAttribute("title",i("media.resizeTimelineHint")),this.paneGrip.setAttribute("aria-valuemin","120"),this.paneGrip.setAttribute("aria-valuemax","720"),this.paneGrip.setAttribute("aria-valuenow",String(this.height||this.viewport.clientHeight)));const e=this.hint?.querySelector("db-button");e?.setAttribute("aria-label",i("media.more")),e?.setAttribute("title",i("media.more"));const s=this.ownerDocument.activeElement?.dataset.focus,r=document.createDocumentFragment(),a=this.node("div","db-vt__ruler"),n=this.node("span","db-vt__corner",i("media.tracks"));a.append(n);const o=[1/this.fps,.1,.25,.5,1,2,5,10,30,60,300,600,3600].find(t=>t*this.scale>=64&&this.duration/t<=200)||3600;for(let t=0;t<=this.duration;t+=o){const e=this.node("span","db-vt__tick",l(t,this.fps));e.style.left=144+t*this.scale+"px",a.append(e)}a.dataset.seek="",r.append(a),this._tracks.forEach((t,e)=>{const s=this.node("div","db-vt__row");s.dataset.track=t.id,s.dataset.kind=t.kind,s.style.setProperty("--db-vt-track-height",`${this.trackHeight(t)}px`),s.dataset.condensed=String(this.trackHeight(t)<44),s.dataset.locked=String(!!t.locked),s.dataset.muted=String(!!t.muted);const a=this.node("div","db-vt__track");a.append(this.node("span","db-vt__track-name",t.label));for(const e of["lock","mute"]){const s=i("lock"===e?t.locked?"media.trackLocked":"media.trackUnlocked":t.muted?"media.soundOff":"media.soundOn"),r=c(`${s} · ${t.label}`,"lock"===e?t.locked?"lock":"unlock":t.muted?"mute":"volume-up",e);r.setAttribute("variant","ghost"),r.dataset.track=t.id,r.setAttribute("aria-pressed",String("lock"===e?!!t.locked:!!t.muted)),r.toggleAttribute("disabled",this.disabled),a.append(r)}s.append(a);const n=this.node("div","db-vt__lane");n.dataset.seek="",n.style.width=this.duration*this.scale+"px",n.style.setProperty("--tick",o*this.scale+"px"),this._clips.filter(e=>e.trackId===t.id).forEach(e=>{const s=this.node("div","db-vt__clip");s.dataset.clip=e.id,s.dataset.focus=e.id,s.dataset.selected=String(this.selectedClipId===e.id),s.setAttribute("role","group"),s.tabIndex=0,s.setAttribute("aria-label",`${e.label}, ${l(e.start,this.fps)}, ${i("media.duration")} ${l(e.duration,this.fps)}`),s.setAttribute("aria-disabled",String(this.disabled||!!t.locked));const r=this.selectedClipId===e.id;s.dataset.appearance=e.appearance||("audio"===(e.kind||t.kind)?"waveform":"standard"),s.dataset.narrow=String(e.duration*this.scale<100),s.dataset.editing=String(this.drag?.clipId===e.id),s.style.left=e.start*this.scale+"px",s.style.width=`${Math.max(24,e.duration*this.scale)}px`,s.dataset.hitProxy=String(e.duration*this.scale<24),s.style.setProperty("--db-vt-actual-width",e.duration*this.scale+"px");for(const a of["start","end"]){const d=this.node("span","db-vt__trim");d.dataset.trim=a,d.dataset.focus=`${e.id}-trim-${a}`,d.tabIndex=!r||this.disabled||t.locked?-1:0,d.setAttribute("role","slider"),d.setAttribute("aria-label",`${i("start"===a?"media.trimStart":"media.trimEnd")} · ${e.label}`),d.setAttribute("aria-valuemin","0"),d.setAttribute("aria-valuemax",String(this.duration)),d.setAttribute("aria-valuenow",String("start"===a?e.start:e.start+e.duration)),d.setAttribute("aria-valuetext",l("start"===a?e.start:e.start+e.duration,this.fps)),d.setAttribute("title",i("start"===a?"media.trimStart":"media.trimEnd")),d.append(this.node("i","db-vt__grip")),s.append(d)}if("filmstrip"===e.appearance&&e.thumbnail){const t=this.node("div","db-vt__filmstrip");t.setAttribute("aria-hidden","true");for(let i=0;i<Math.min(24,Math.ceil(e.duration*this.scale/64));i++){const i=document.createElement("img");i.src=e.thumbnail,i.alt="",i.draggable=!1,t.append(i)}s.append(t)}for(const a of["in","out"]){const n=d("in"===a?e.fadeIn||0:e.fadeOut||0,0,e.duration),o=this.node("span","db-vt__fade");if(o.dataset.fadeArea=a,o.style.width=n*this.scale+"px",o.setAttribute("aria-hidden","true"),s.append(o),r){const r=this.node("span","db-vt__fade-handle");r.dataset.fade=a,r.dataset.focus=`${e.id}-fade-${a}`,r.style["in"===a?"left":"right"]=n*this.scale+"px",r.tabIndex=this.disabled||t.locked?-1:0,r.setAttribute("role","slider"),r.setAttribute("aria-label",`${i("in"===a?"media.fadeIn":"media.fadeOut")} · ${e.label}`),r.setAttribute("aria-valuemin","0"),r.setAttribute("aria-valuemax",String(e.duration)),r.setAttribute("aria-valuenow",String(n)),r.setAttribute("title",i("in"===a?"media.fadeIn":"media.fadeOut")),s.append(r)}}if(s.append(this.node("span","db-vt__clip-label",e.label)),"audio"===(e.kind||t.kind)||"waveform"===e.appearance){const t=this.node("span","db-vt__wave");t.setAttribute("aria-hidden","true");for(let i=0;i<Math.min(160,Math.ceil(e.duration*this.scale/7));i++){const e=this.node("i","");e.style.height=20+75*Math.abs(Math.sin(1.7*i)*Math.cos(.4*i))+"%",t.append(e)}s.append(t)}if(e.effects?.length){const t=this.node("span","db-vt__effects");t.setAttribute("aria-label",i("media.effects"));for(const i of e.effects){const e=this.node("span","db-vt__effect",i.label);e.dataset.enabled=String(!1!==i.enabled),e.dataset.kind=i.kind,e.setAttribute("title",i.label),t.append(e)}s.append(t)}n.append(s)}),s.append(n);const h=this.node("div","db-vt__track-resize");h.dataset.trackResize=t.id,h.tabIndex=0,h.setAttribute("role","separator"),h.setAttribute("aria-orientation","horizontal"),h.setAttribute("aria-label",`${i("media.resizeTrack")} · ${t.label}`),h.setAttribute("aria-valuemin","32"),h.setAttribute("aria-valuemax","120"),h.setAttribute("aria-valuenow",String(this.trackHeight(t))),s.append(h),r.append(s),this.renderEffects(r,t)});for(const t of this._markers){const e=this.node("button","db-vt__marker");e.type="button",e.dataset.marker=String(t.time),e.style.left=144+t.time*this.scale+"px";const s=`${i("media.marker")} · ${t.label} · ${l(t.time,this.fps)}`;e.setAttribute("aria-label",s),e.setAttribute("title",s),r.append(e)}if(this.cursor=this.node("div","db-vt__playhead"),this.cursor.dataset.playhead="",this.cursor.tabIndex=0,this.cursor.setAttribute("role","slider"),this.cursor.dataset.focus="playhead",this.cursor.setAttribute("aria-label",i("media.playhead")),this.cursor.setAttribute("aria-valuemin","0"),this.cursor.setAttribute("aria-valuemax",String(this.duration)),this.cursor.setAttribute("aria-disabled",String(this.disabled)),r.append(this.cursor),this._tracks.length||r.append(this.node("p","db-vt__empty",i("media.emptyTracks"))),this.canvas.style.width=144+this.duration*this.scale+"px",this.drag?.clipId){const t=this._clips.find(t=>t.id===this.drag?.clipId);if(t){const e="fade-in"===this.drag.kind?t.fadeIn||0:"fade-out"===this.drag.kind?t.fadeOut||0:"trim-end"===this.drag.kind?t.start+t.duration:t.start,s=this.node("div","db-vt__readout",`${i("media."+("trim-start"===this.drag.kind?"trimStart":"trim-end"===this.drag.kind?"trimEnd":"fade-in"===this.drag.kind?"fadeIn":"fade-out"===this.drag.kind?"fadeOut":"time"))} ${l(e,this.fps)} / ${i("media.duration")} ${l(t.duration,this.fps)}`);this.append(s)}}this.canvas.replaceChildren(r),this.viewport.setAttribute("aria-label",i("media.timeline")),this.syncCursor(),this.syncHint(),s&&!this.drag&&Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===s)?.focus({preventScroll:!0})}renderEffects(t,s){for(const r of this._clips.filter(t=>t.trackId===s.id&&t.effects?.some(t=>t.properties?.length))){const n=this.node("section","db-vt__effect-tree");n.dataset.effectClip=r.id;const toggle=(t,e,i,s)=>{const r=c(t,i?"chevron-down":"chevron-right","effect-disclosure");return r.removeAttribute("icon-only"),r.setAttribute("variant","ghost"),r.textContent=t,r.setAttribute("aria-expanded",String(i)),r.setAttribute("aria-controls",e),r.dataset.focus=e,r.addEventListener("db-click",t=>{t.stopPropagation(),s(),this.render()}),r},o=`${this.effectTreeId}-${r.id}`;n.append(toggle(`${r.label} · ${i("media.effects")}`,o,this.expandedEffects.has(r.id),()=>{this.expandedEffects.has(r.id)?this.expandedEffects.delete(r.id):this.expandedEffects.add(r.id)}));const f=this.node("div","db-vt__effect-content");if(f.id=o,f.hidden=!this.expandedEffects.has(r.id),!f.hidden)for(const t of r.effects||[]){if(!t.properties?.length)continue;const i=`${r.id}/${t.id}`,n=`${o}-${t.id}`;f.append(toggle(t.label,n,this.expandedEffects.has(i),()=>{this.expandedEffects.has(i)?this.expandedEffects.delete(i):this.expandedEffects.add(i)}));const p=this.node("div","db-vt__property-group");if(p.id=n,p.hidden=!this.expandedEffects.has(i),!p.hidden)for(const n of t.properties){const o=`${i}/${n.id}`,f=this.node("div","db-vt__property-row");f.dataset.effectProperty=n.id,f.dataset.effectId=t.id;const u=this.node("div","db-vt__property-label"),m=c(n.label,"settings","effect-property");m.removeAttribute("icon-only"),m.setAttribute("variant","ghost"),m.textContent=n.label,m.setAttribute("aria-expanded",String(this.activeProperty.get(r.id)===o)),m.dataset.focus=o,m.addEventListener("db-click",t=>{t.stopPropagation(),this.activeProperty.set(r.id,this.activeProperty.get(r.id)===o?"":o),this.render()}),u.append(m,this.node("span","db-vt__property-value",String(Math.round(100*h(n.keyframes,this.currentTime-r.start))/100))),f.append(u);const k=this.node("div","db-vt__property-lane");k.style.width=this.duration*this.scale+"px";for(const e of n.keyframes){const i=this.node("button","db-vt__property-key");i.dataset.propertyKey=e.id,i.dataset.keyToken=this.effectToken(r.id,t.id,n.id,e.id),i.setAttribute("aria-pressed",String(this.selectedEffectKeys.has(i.dataset.keyToken))),i.addEventListener("pointerdown",s=>{if(i.disabled||0!==s.button)return;s.preventDefault(),s.stopPropagation(),this.viewport?.focus({preventScroll:!0});const a=i.dataset.keyToken;Array.from(this.selectedEffectKeys).some(t=>JSON.parse(t)[0]!==r.id)&&this.selectedEffectKeys.clear(),s.shiftKey?this.selectedEffectKeys.has(a)?this.selectedEffectKeys.delete(a):this.selectedEffectKeys.add(a):this.selectedEffectKeys.has(a)||(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(a)),this.syncEffectSelection(),this.selectedEffectKeys.has(a)?(this.effectDrag={pointer:s.pointerId,clipId:r.id,effectId:t.id,propertyId:n.id,keyId:e.id,x:s.clientX,time:e.time,before:this.clips,changed:!1,copies:s.altKey?this.makeKeyCopyIds():void 0},this.effectEditing=!0,this.setPointerCapture(s.pointerId)):this.render()}),i.type="button",i.style.left=(r.start+e.time)*this.scale+"px",i.setAttribute("aria-label",`${n.label} · ${l(e.time,this.fps)} · ${e.value}`),i.disabled=this.disabled||!!s.locked||!1===t.enabled,i.addEventListener("click",t=>{t.stopPropagation(),t.shiftKey||(this.selectedEffectKeys.has(i.dataset.keyToken)||(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(i.dataset.keyToken)),this.activeProperty.set(r.id,o),this.effectKeySelection.set(o,e.id),this.seek(r.start+e.time),this.render())}),k.append(i)}if(f.append(k),p.append(f),this.activeProperty.get(r.id)===o){const i=new e;i.dataset.effectEditor=n.id,i.duration=r.duration,i.fps=this.fps,i.min=n.min,i.max=n.max,i.setAttribute("label",n.label),i.keyframes=n.keyframes,i.selectedKeyframeId=this.effectKeySelection.get(o)||n.keyframes[0]?.id||"",i.currentTime=d(this.currentTime-r.start,0,r.duration),i.disabled=this.disabled||!!s.locked||!1===t.enabled;const edit=e=>{e.stopPropagation();const i=e.detail;this.effectEditing="db-input"===e.type&&!i.cancelled;const s=i.keyframes;this._clips=this._clips.map(e=>e.id!==r.id?e:{...e,effects:e.effects?.map(e=>e.id!==t.id?e:{...e,properties:e.properties?.map(t=>t.id!==n.id?t:{...t,keyframes:s})})}),a(this,e.type,{clips:this.clips,clipId:r.id,action:"keyframe",effectId:t.id,propertyId:n.id,...i.cancelled?{cancelled:!0}:{}}),("db-change"===e.type||i.cancelled)&&(this.effectEditing=!1,this.render())};i.addEventListener("db-input",edit),i.addEventListener("db-change",edit),i.addEventListener("db-selection-change",t=>{t.stopPropagation(),this.effectKeySelection.set(o,t.detail.keyframeId)}),i.addEventListener("db-seek",t=>{t.stopPropagation(),this.seek(r.start+t.detail.time)});const c=this.node("div","db-vt__effect-editor");c.style.width=`${Math.max(240,(this.viewport?.clientWidth||800)-16)}px`,c.append(i),p.append(c)}}f.append(p)}n.addEventListener("pointerdown",t=>{if(0!==t.button||this.disabled||s.locked||t.target.closest("db-button,db-keyframe-editor,db-input,db-select,.db-vt__property-label,[data-property-key]"))return;t.preventDefault(),t.stopPropagation(),this.viewport?.focus({preventScroll:!0});const e=this.node("div","db-vt__marquee");this.append(e),this.marquee={pointer:t.pointerId,x:t.clientX,y:t.clientY,clipId:r.id,before:t.shiftKey?new Set(Array.from(this.selectedEffectKeys).filter(t=>JSON.parse(t)[0]===r.id)):new Set,box:e},t.shiftKey||this.selectedEffectKeys.clear(),this.syncEffectSelection(),this.setPointerCapture(t.pointerId)}),n.append(f),t.append(n)}}select(t){this.selectedClipId!==t&&(this.selectedClipId=t,a(this,"db-selection-change",{clipId:t}))}timeAt(t){return(t-this.viewport.getBoundingClientRect().left+this.viewport.scrollLeft-144)/this.scale}snapped(t,e){let i=r(t,this.fps);if(this.snap&&!this.drag?.shift){const t=[0,this.currentTime,...this._markers.map(t=>t.time),...this._clips.filter(t=>t.id!==e).flatMap(t=>[t.start,t.start+t.duration])].reduce((t,e)=>Math.abs(e-i)<Math.abs(t-i)?e:t,1/0);Math.abs(t-i)*this.scale<=8&&(i=r(t,this.fps))}return i}updateDrag(){const t=this.drag;if(!t)return;if("seek"===t.kind)return void this.seek(this.timeAt(t.x));const e=t.before.find(e=>e.id===t.clipId);if(!e)return;const i=(t.x-t.originX+this.viewport.scrollLeft-t.scroll)/this.scale;let s=e.start,n=e.duration,o=e.sourceOffset||0,c=e.trackId;if("fade-in"===t.kind||"fade-out"===t.kind){const s="fade-in"===t.kind?"fadeIn":"fadeOut",n="fade-in"===t.kind?e.fadeOut||0:e.fadeIn||0,o=r(d((e[s]||0)+("fade-in"===t.kind?i:-i),0,Math.max(0,e.duration-n)),this.fps);return this._clips=t.before.map(t=>t.id===e.id?{...t,[s]:o}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),void a(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}if("move"===t.kind){s=this.snapped(s+i,e.id),this.canvas.getBoundingClientRect();const r=this.trackIndexAt(t.y),a=this._tracks[d(r,0,this._tracks.length-1)],o=this._tracks.find(t=>t.id===e.trackId);!a||a.locked||"mixed"!==a.kind&&a.kind!==(e.kind||o?.kind)||(c=a.id),s=d(s,0,this.duration-n)}else if("trim-start"===t.kind)s=d(this.snapped(s+i,e.id),Math.max(0,e.start-o),e.start+n-Math.min(this.minClipDuration,e.start+n-Math.max(0,e.start-o))),n=e.start+e.duration-s,o+=s-e.start;else{const t=e.sourceDuration?Math.min(this.duration-s,e.sourceDuration-o):this.duration-s;n=d(this.snapped(s+n+i,e.id)-s,Math.min(this.minClipDuration,t),t)}const h=t.ripple&&("trim-start"===t.kind||"trim-end"===t.kind),l=t.before.filter(t=>t.id!==e.id&&t.trackId===e.trackId&&t.start>=e.start+e.duration-1e-6);let f=0;if(h){"trim-start"===t.kind&&(s=e.start);const i=Math.max(e.start+e.duration,...l.map(t=>t.start+t.duration));n=Math.min(n,e.duration+this.duration-i),f=r(n-e.duration,this.fps)}let p=t.before.map(t=>l.some(e=>e.id===t.id)&&h?{...t,start:r(t.start+f,this.fps)}:t);if("move"===t.kind&&t.ripple){const i=t.before.filter(i=>i.trackId===c&&(t.duplicate||i.id!==e.id)).sort((t,e)=>t.start-e.start),a=i.find(t=>s>t.start+1e-6&&s<t.start+t.duration-1e-6);a&&(s=a.start);let d=s+n;const o=new Map;for(const t of i){if(t.start<s-1e-6)continue;const e=Math.max(t.start,d);o.set(t.id,e),d=e+t.duration}if(d>this.duration+1e-6)return;p=t.before.map(t=>o.has(t.id)?{...t,start:r(o.get(t.id),this.fps)}:t)}if(p.some(i=>(t.duplicate||i.id!==e.id)&&i.trackId===c&&s<i.start+i.duration-1e-6&&s+n>i.start+1e-6))return;const u=t.duplicate||e.id;t.duplicate&&(p=[...p,{...e,id:u}]),this._clips=p.map(t=>t.id===u?{...t,kind:t.kind||("mixed"===this._tracks.find(t=>t.id===e.trackId)?.kind?"video":this._tracks.find(t=>t.id===e.trackId)?.kind),start:r(s,this.fps),duration:r(n,this.fps),sourceOffset:r(o,this.fps),trackId:c,...void 0!==t.fadeIn?{fadeIn:Math.min(t.fadeIn,n)}:{},...void 0!==t.fadeOut?{fadeOut:Math.min(t.fadeOut,Math.max(0,n-(t.fadeIn||0)))}:{}}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),a(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}restoreTimelineHistory(t=!1){const e=t?this.redoClips:this.undoClips,i=t?this.undoClips:this.redoClips,s=e.pop();s&&(i.push(this.clips),this._clips=structuredClone(s),this.committedClips=this.clips,this.selectedEffectKeys.clear(),this.render(),a(this,"db-change",{clips:this.clips,clipId:this.selectedClipId,action:t?"redo":"undo"}))}makeKeyCopyIds(){return new Map(Array.from(this.selectedEffectKeys,t=>[t,`key-${crypto.randomUUID()}`]))}copyKeyData(t,e,i,s){let a=!0,d=0;const n=t.map(t=>t.id!==e?t:{...t,effects:t.effects?.map(e=>({...e,properties:e.properties?.map(n=>{const o=n.keyframes.filter(i=>s.has(this.effectToken(t.id,e.id,n.id,i.id))).map(o=>{const c=r(o.time+i,this.fps);return d++,(c<0||c>t.duration||n.keyframes.some(t=>Math.abs(t.time-c)<.5/this.fps))&&(a=!1),{...structuredClone(o),id:s.get(this.effectToken(t.id,e.id,n.id,o.id)),time:c}});return{...n,keyframes:[...n.keyframes,...o].sort((t,e)=>t.time-e.time)}})}))});return a&&d?n:void 0}duplicateSelectedKeys(t){const e=this.makeKeyCopyIds(),i=this.clips;for(let s=1;s<=Math.min(3e3,Math.ceil(this.duration*this.fps));s++)for(const r of[1,-1]){const d=this.copyKeyData(i,t,r*s/this.fps,e);if(d)return this._clips=d,this.selectedEffectKeys=new Set(Array.from(e,([t,e])=>{const[i,s,r]=JSON.parse(t);return this.effectToken(i,s,r,e)})),a(this,"db-change",{clips:this.clips,clipId:t,action:"keyframe"}),void this.render()}}effectToken(t,e,i,s){return JSON.stringify([t,e,i,s])}syncEffectSelection(){for(const t of this.querySelectorAll("[data-key-token]"))t.setAttribute("aria-pressed",String(this.selectedEffectKeys.has(t.dataset.keyToken)))}finishMarquee(t=!1){const e=this.marquee;e&&(this.marquee=void 0,e.box.remove(),t&&(this.selectedEffectKeys=e.before),this.hasPointerCapture(e.pointer)&&this.releasePointerCapture(e.pointer),this.render())}editSelectedKeys(t,e){const i=this._clips.find(e=>e.id===t);!i||this.disabled||this._tracks.find(t=>t.id===i.trackId)?.locked||(this._clips=this._clips.map(i=>i.id!==t?i:{...i,effects:i.effects?.map(t=>!1===t.enabled?t:{...t,properties:t.properties?.map(s=>{let r=s.keyframes;if(e)for(const a of s.keyframes)this.selectedEffectKeys.has(this.effectToken(i.id,t.id,s.id,a.id))&&(r=f(r,a.id,e));else r=r.filter(e=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,s.id,e.id)));return{...s,keyframes:r}})})}),e||this.selectedEffectKeys.clear(),a(this,"db-change",{clips:this.clips,clipId:t,action:"keyframe"}),this.render(),this.viewport?.focus({preventScroll:!0}))}finishEffectDrag(t=!1){const e=this.effectDrag;e&&(this.effectDrag=void 0,this.effectEditing=!1,this.hasPointerCapture(e.pointer)&&this.releasePointerCapture(e.pointer),t?this._clips=e.before:e.copies&&e.changed&&(this.selectedEffectKeys=new Set(Array.from(e.copies,([t,e])=>{const[i,s,r]=JSON.parse(t);return this.effectToken(i,s,r,e)}))),(t||e.changed)&&a(this,t?"db-input":"db-change",{clips:this.clips,clipId:e.clipId,action:"keyframe",effectId:e.effectId,propertyId:e.propertyId,...t?{cancelled:!0}:{}}),this.render())}finish(t=!1){const e=this.drag;e&&(cancelAnimationFrame(this.raf),this.drag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._clips=e.before,this.render(),a(this,"db-input",{clips:this.clips,clipId:e.clipId,action:e.kind,cancelled:!0})):e.changed&&e.clipId&&"seek"!==e.kind&&a(this,"db-change",{clips:this.clips,clipId:e.clipId,action:e.kind}),this.render(),this.viewport?.focus({preventScroll:!0}))}rollback(){this.finishMarquee(!0),this.finishEffectDrag(!0),this.finish(!0)}splitSelected(){const t=this._clips.find(t=>t.id===this.selectedClipId);if(this.disabled||!t||this._tracks.find(e=>e.id===t.trackId)?.locked||t.duration<2*this.minClipDuration||this.currentTime<t.start+this.minClipDuration||this.currentTime>t.start+t.duration-this.minClipDuration)return;const e=this.currentTime;let i=t.id+"-split";for(;this._clips.some(t=>t.id===i);)i+="-1";this._clips=this._clips.flatMap(s=>s.id===t.id?[{...s,duration:e-s.start,fadeOut:0,fadeIn:Math.min(s.fadeIn||0,e-s.start)},{...s,id:i,fadeIn:0,fadeOut:Math.min(s.fadeOut||0,s.start+s.duration-e),start:e,duration:s.start+s.duration-e,sourceOffset:(s.sourceOffset||0)+e-s.start}]:[s]),this.render(),a(this,"db-change",{clips:this.clips,action:"split",clipId:t.id})}trackIndexAt(t){const e=Array.from(this.canvas.querySelectorAll(".db-vt__row"));let i=0;return e.forEach((e,s)=>{t>=e.getBoundingClientRect().top&&(i=s)}),i}finishResize(t=!1){const e=this.resizeDrag;e&&(this.resizeDrag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._tracks=e.beforeTracks,this.height=e.beforeHeight,this.render(),a(this,e.trackId?"db-track-input":"db-resize-input",e.trackId?{tracks:this.tracks,cancelled:!0}:{height:this.height,cancelled:!0})):a(this,e.trackId?"db-track-change":"db-resize",e.trackId?{tracks:this.tracks}:{height:this.height}))}changeZoom(t,e,i="keyboard"){if(!this.viewport)return;const s=this.zoom,r=e??Math.min(this.viewport.clientWidth-8,Math.max(152,144+this.currentTime*this.scale-this.viewport.scrollLeft)),n=(this.viewport.scrollLeft+r-144)/this.scale;this.zoom=Math.round(100*d(t,10,800))/100,this.viewport.scrollLeft="fit"===i?0:Math.max(0,144+n*this.scale-r),this.zoom!==s&&a(this,"db-zoom-change",{value:this.zoom,previousValue:s,anchorTime:n,source:i})}}customElements.get("db-video-timeline")||customElements.define("db-video-timeline",DbVideoTimeline);export{DbVideoTimeline};
|
|
1
|
+
import"./db-button.js";import{DbContextMenu as t}from"./db-context-menu.js";import{DbKeyframeEditor as e}from"./db-keyframe-editor.js";import{t as i,subscribeDbLocale as s}from"../core/i18n.js";import{frameTime as r,clamp as a,emitEditor as d,editorEditable as n,finite as o,editorButton as h,evaluateKeyframes as c,editorTimecode as l,applyKeyframeEasing as p}from"./media-editor-model.js";class DbVideoTimeline extends HTMLElement{constructor(){super(...arguments),this.effectTreeId=`effects-${crypto.randomUUID()}`,this.effectKeySelection=new Map,this.expandedEffects=new Set,this.activeProperty=new Map,this.effectEditing=!1,this.undoClips=[],this.redoClips=[],this.committedClips=[],this.selectedEffectKeys=new Set,this._tracks=[],this._clips=[],this._markers=[],this.raf=0,this.down=t=>{if(this.drag||this.resizeDrag)return;if(this.disabled||0!==t.button||!(t.target instanceof Element)||t.target.closest("db-button,db-keyframe-editor,[data-effect-clip]"))return;const e=t.target.closest("[data-track-resize]"),i=t.target.closest("[data-pane-resize]");if(e||i){t.preventDefault(),this.viewport?.focus({preventScroll:!0});const i=this._tracks.find(t=>t.id===e?.dataset.trackResize);return this.resizeDrag={id:t.pointerId,trackId:i?.id,startY:t.clientY,initial:i?this.trackHeight(i):this.viewport.getBoundingClientRect().height,beforeTracks:this.tracks,beforeHeight:this.height},void this.setPointerCapture(t.pointerId)}const s=t.target.closest("[data-marker]");if(s)return void this.seek(Number(s.dataset.marker));const r=t.target.closest("[data-clip]"),a=this._clips.find(t=>t.id===r?.dataset.clip);if(a){if(this.selectedEffectKeys.clear(),this.select(a.id),this._tracks.find(t=>t.id===a.trackId)?.locked)return;this.viewport?.focus({preventScroll:!0})}else if(!t.target.closest("[data-seek],[data-playhead]"))return;if("touch"===t.pointerType&&!a&&!t.target.closest(".db-vt__ruler,[data-playhead]"))return;t.preventDefault();const d=t.target.closest("[data-trim]")?.dataset.trim,n=t.target.closest("[data-fade]")?.dataset.fade;this.drag={kind:a?"in"===n?"fade-in":"out"===n?"fade-out":"start"===d?"trim-start":"end"===d?"trim-end":"move":"seek",id:t.pointerId,clipId:a?.id,originX:t.clientX,originY:t.clientY,scroll:this.viewport.scrollLeft,before:this.clips,x:t.clientX,y:t.clientY,changed:!1,shift:t.shiftKey,ripple:t.metaKey||t.ctrlKey,duplicate:a&&!d&&!n&&t.altKey?`clip-${crypto.randomUUID()}`:void 0},this.setPointerCapture(t.pointerId),a||this.seek(this.timeAt(t.clientX)),this.raf=requestAnimationFrame(this.autoScroll)},this.move=t=>{if(this.rangeDrag?.pointer===t.pointerId){const e=this.rangeDrag,i=e.before.find(t=>t.id===e.clipId),s=i.effects.find(t=>t.id===e.effectId),n=r((t.clientX-e.x+(this.viewport?.scrollLeft||0)-e.scroll)/this.scale,this.fps),o=Math.max(0,s.start||0),h=Math.max(1/this.fps,s.duration??i.duration);let c=o,l=h;"move"===e.kind?c=Math.max(0,c+n):"start"===e.kind?(c=a(c+n,0,c+l-1/this.fps),l=o+h-c):l=Math.max(1/this.fps,l+n),this._clips=e.before.map(t=>t.id!==i.id?t:{...t,effects:t.effects?.map(t=>t.id!==s.id?t:{...t,start:r(c,this.fps),duration:r(l,this.fps)})});const p=this.querySelector(`[data-effect-clip="${CSS.escape(i.id)}"] [data-range-effect="${CSS.escape(s.id)}"]`);return p&&(p.style.left=(i.start+c)*this.scale+"px",p.style.width=`${Math.max(12,l*this.scale)}px`),void d(this,"db-input",{clips:this.clips,clipId:i.id,effectId:s.id,action:"effect-range"})}if(this.labelDrag?.id!==t.pointerId){if(this.marquee?.pointer===t.pointerId){const e=this.marquee,i=this.getBoundingClientRect(),s=Math.min(e.x,t.clientX),r=Math.min(e.y,t.clientY),a=Math.max(e.x,t.clientX),d=Math.max(e.y,t.clientY);Object.assign(e.box.style,{left:s-i.left+"px",top:r-i.top+"px",width:a-s+"px",height:d-r+"px"}),this.selectedEffectKeys=new Set(e.before);for(const t of this.querySelectorAll("[data-key-token]")){if(t.closest("[data-effect-clip]")?.dataset.effectClip!==e.clipId||t.hasAttribute("disabled"))continue;const i=t.getBoundingClientRect(),n=i.x+i.width/2,o=i.y+i.height/2;n>=s&&n<=a&&o>=r&&o<=d&&this.selectedEffectKeys.add(t.dataset.keyToken)}return void this.syncEffectSelection()}if(this.effectDrag?.pointer===t.pointerId){const e=this.effectDrag,i=e.before.find(t=>t.id===e.clipId);if(t.altKey&&!e.copies&&(e.copies=this.makeKeyCopyIds()),e.copies){const s=r((t.clientX-e.x)/this.scale,this.fps);if(0===s){this._clips=e.before,e.changed=!1;for(const t of this.querySelectorAll("[data-duplicate-preview]"))t.remove();return void this.syncEffectSelection()}const a=this.copyKeyData(e.before,e.clipId,s,e.copies);if(!a)return;this._clips=a,e.changed=!0;for(const[t,s]of e.copies){const[e,r,d]=JSON.parse(t),n=this.effectToken(e,r,d,s),o=Array.from(this.querySelectorAll("[data-key-token]")).find(e=>e.dataset.keyToken===t);if(!o)continue;let h=Array.from(this.querySelectorAll("[data-key-token]")).find(t=>t.dataset.keyToken===n);h||(h=o.cloneNode(!0),h.dataset.keyToken=n,h.dataset.propertyKey=s,h.dataset.duplicatePreview="true",o.parentElement.append(h));const c=a.find(t=>t.id===e)?.effects?.find(t=>t.id===r)?.properties?.find(t=>t.id===d)?.keyframes.find(t=>t.id===s);c&&(h.style.left=(i.start+c.time)*this.scale+"px"),h.setAttribute("aria-pressed","true"),o.setAttribute("aria-pressed","false")}return void d(this,"db-input",{clips:this.clips,clipId:e.clipId,action:"keyframe"})}let s=-1/0,n=1/0;for(const t of i.effects||[])for(const e of t.properties||[]){const r=[...e.keyframes].sort((t,e)=>t.time-e.time);r.forEach((a,d)=>{if(!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,a.id)))return;const o=r.slice(0,d).reverse().find(s=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,s.id))),h=r.slice(d+1).find(s=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,e.id,s.id)));s=Math.max(s,(o?o.time+1/this.fps:0)-a.time),n=Math.min(n,(h?h.time-1/this.fps:i.duration)-a.time)})}const o=r(a((t.clientX-e.x)/this.scale,s,n),this.fps);this._clips=e.before.map(t=>t.id!==e.clipId?t:{...t,effects:t.effects?.map(e=>({...e,properties:e.properties?.map(i=>({...i,keyframes:i.keyframes.map(s=>this.selectedEffectKeys.has(this.effectToken(t.id,e.id,i.id,s.id))?{...s,time:r(s.time+o,this.fps)}:s)}))}))}),e.changed=0!==o;for(const t of this.querySelectorAll("[data-key-token]")){if(!this.selectedEffectKeys.has(t.dataset.keyToken))continue;const[e,i,s,r]=JSON.parse(t.dataset.keyToken),a=this._clips.find(t=>t.id===e),d=a?.effects?.find(t=>t.id===i)?.properties?.find(t=>t.id===s)?.keyframes.find(t=>t.id===r);a&&d&&(t.style.left=(a.start+d.time)*this.scale+"px")}return void d(this,"db-input",{clips:this.clips,clipId:e.clipId,action:"keyframe",effectId:e.effectId,propertyId:e.propertyId})}if(this.resizeDrag?.id===t.pointerId){const e=this.resizeDrag,i=Math.round(e.initial+t.clientY-e.startY);return void(e.trackId?(this._tracks=e.beforeTracks.map(t=>t.id===e.trackId?{...t,height:a(i,32,120)}:t),this.render(),d(this,"db-track-input",{tracks:this.tracks})):(this.height=a(i,120,720),d(this,"db-resize-input",{height:this.height})))}this.drag?.id===t.pointerId&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.drag.shift=t.shiftKey,this.drag.ripple=t.metaKey||t.ctrlKey,"move"===this.drag.kind&&t.altKey&&!this.drag.duplicate&&(this.drag.duplicate=`clip-${crypto.randomUUID()}`),this.updateDrag())}else this.labelWidth=this.labelDrag.width+t.clientX-this.labelDrag.x},this.autoScroll=()=>{if(!this.drag||!this.viewport)return;const t=this.viewport.getBoundingClientRect(),e=this.drag.x,i=e>t.right-48?a((e-t.right+48)/3,0,18):e<t.left+this.labelWidth+32?-a((t.left+176-e)/3,0,18):0,s=this.viewport.scrollLeft;this.viewport.scrollLeft+=i,s!==this.viewport.scrollLeft&&this.updateDrag(),this.raf=requestAnimationFrame(this.autoScroll)},this.recordTimelineHistory=t=>{if(t.target!==this)return;const e=t.detail;e?.clips&&"undo"!==e.action&&"redo"!==e.action&&JSON.stringify(e.clips)!==JSON.stringify(this.committedClips)&&(this.undoClips.push(structuredClone(this.committedClips)),this.undoClips.length>100&&this.undoClips.shift(),this.redoClips=[],this.committedClips=structuredClone(e.clips))},this.effectContextMenu=e=>{const s=e.target,r=s.closest("[data-effect-clip]");if(!r)return;const a=this._clips.find(t=>t.id===r.dataset.effectClip);if(!a)return;e.preventDefault(),e.stopPropagation(),this.viewport?.focus({preventScroll:!0});const d=s.closest("[data-key-token]");d&&!this.selectedEffectKeys.has(d.dataset.keyToken)&&(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(d.dataset.keyToken),this.syncEffectSelection());const n=this.disabled||!!this._tracks.find(t=>t.id===a.trackId)?.locked||!this.selectedEffectKeys.size||!!d?.hasAttribute("disabled");this.keyMenu||(this.keyMenu=new t,this.append(this.keyMenu)),this.keyMenu.items=[{id:"duplicate",label:i("media.duplicateKeys"),icon:"copy",disabled:n},{id:"easing",label:i("media.keyEasing"),disabled:n,subItems:["linear","hold","ease-in","ease-out","ease-in-out"].map(t=>({id:`ease:${t}`,label:i("media."+t)}))},{id:"divider",divider:!0},{id:"delete",label:i("media.deleteKey"),icon:"trash",variant:"danger",disabled:n},{id:"undo",label:i("media.undo"),disabled:!this.undoClips.length}],this.keyMenu.oncontextmenu=t=>t.preventDefault(),this.keyMenu.addEventListener("db-select",this.keyMenuSelect),this.keyMenu.dataset.clipId=a.id,this.keyMenu.openAt(e.clientX,e.clientY)},this.keyMenuSelect=t=>{t.stopPropagation();const e=t.detail.item.id,i=this.keyMenu.dataset.clipId;"duplicate"===e?this.duplicateSelectedKeys(i):"delete"===e?this.editSelectedKeys(i):e.startsWith("ease:")?this.editSelectedKeys(i,e.slice(5)):"undo"===e&&this.restoreTimelineHistory()},this.up=t=>{if(this.rangeDrag?.pointer!==t.pointerId)return this.labelDrag?.id===t.pointerId?(this.labelDrag=void 0,this.hasPointerCapture(t.pointerId)&&this.releasePointerCapture(t.pointerId),void d(this,"db-label-resize",{width:this.labelWidth})):void(this.marquee?.pointer!==t.pointerId?this.effectDrag?.pointer!==t.pointerId?this.resizeDrag?.id!==t.pointerId?this.drag?.id===t.pointerId&&(this.drag.changed&&(this.drag.x=t.clientX,this.drag.y=t.clientY,this.updateDrag()),this.finish()):this.finishResize():this.finishEffectDrag():this.finishMarquee());this.rangeFinish()},this.cancel=t=>{if(this.labelDrag?.id===t.pointerId){if("lostpointercapture"===t.type&&this.hasPointerCapture(t.pointerId))return;const e=this.labelDrag;return this.labelDrag=void 0,void(this.labelWidth=e.width)}if(this.rangeDrag?.pointer===t.pointerId){if("lostpointercapture"===t.type&&(t.target!==this||this.hasPointerCapture(t.pointerId)))return;return void this.rangeFinish(!0)}[this.drag?.id,this.resizeDrag?.id,this.effectDrag?.pointer,this.marquee?.pointer].includes(t.pointerId)&&("lostpointercapture"!==t.type||t.target===this&&!this.hasPointerCapture(t.pointerId))&&(this.finishMarquee(!0),this.finishEffectDrag(!0),this.finish(!0),this.finishResize(!0))},this.onActionClick=t=>{const e=t.target,i=e.dataset.action;if(!this.disabled&&("scroll"===i&&this.viewport?.scrollBy({left:Math.max(160,this.viewport.clientWidth-this.labelWidth),behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"instant":"smooth"}),"lock"===i||"mute"===i)){const t=e.dataset.track;this._tracks=this._tracks.map(e=>e.id===t?{...e,["lock"===i?"locked":"muted"]:!e["lock"===i?"locked":"muted"]}:e),this.render(),d(this,"db-track-change",{tracks:this.tracks})}},this.doubleResize=t=>{if(this.disabled||!(t.target instanceof Element))return;const e=t.target.closest("[data-track-resize],[data-pane-resize]");e&&(e.dataset.trackResize?(this._tracks=this._tracks.map(t=>t.id===e.dataset.trackResize?{...t,height:this.rowHeight}:t),this.render(),d(this,"db-track-change",{tracks:this.tracks})):(this.height=0,d(this,"db-resize",{height:0})))},this.wheel=t=>{if(this.disabled||!this.shortcuts||this.drag||this.resizeDrag||!t.metaKey&&!t.ctrlKey)return;t.preventDefault(),t.stopPropagation();const e=Math.exp(-t.deltaY*(1===t.deltaMode?.04:.002));this.changeZoom(this.zoom*e,t.clientX-this.viewport.getBoundingClientRect().left,"wheel")},this.key=t=>{if(!this.disabled&&!n(t.target)&&(t.metaKey||t.ctrlKey)&&"z"===t.key.toLowerCase()&&!this.drag&&!this.effectDrag&&!this.effectEditing)return void((t.shiftKey?this.redoClips:this.undoClips).length&&(t.preventDefault(),t.stopPropagation(),this.restoreTimelineHistory(t.shiftKey)));if(!(this.disabled||n(t.target)||t.metaKey||t.ctrlKey||t.altKey||"u"!==t.key.toLowerCase())){const e=this._clips.find(t=>t.id===this.selectedClipId&&t.effects?.some(t=>t.properties?.length))||this._clips.find(t=>t.effects?.some(t=>t.properties?.length));if(e){t.preventDefault(),t.stopPropagation();const i=!this.expandedEffects.has(e.id);for(const t of[e.id,...(e.effects||[]).map(t=>`${e.id}/${t.id}`)])i?this.expandedEffects.add(t):this.expandedEffects.delete(t);this.render(),this.viewport?.focus({preventScroll:!0})}return}if("Escape"===t.key&&this.rangeDrag)return t.preventDefault(),void this.rangeFinish(!0);if("Escape"===t.key&&this.labelDrag){t.preventDefault();const e=this.labelDrag;return this.labelDrag=void 0,this.labelWidth=e.width,void(this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id))}if("Escape"===t.key&&this.marquee)return t.preventDefault(),void this.finishMarquee(!0);if(!this.disabled&&!n(t.target)&&!t.target?.closest("db-keyframe-editor")&&["Delete","Backspace"].includes(t.key)&&this.selectedEffectKeys.size){t.preventDefault();const e=new Set(Array.from(this.selectedEffectKeys).map(t=>JSON.parse(t)[0]));for(const t of e)this.editSelectedKeys(t);return}if(this.disabled||n(t.target)||t.isComposing||t.target?.closest("db-keyframe-editor"))return;if("Escape"===t.key&&this.effectDrag)return t.preventDefault(),void this.finishEffectDrag(!0);if("Escape"===t.key&&this.drag)return t.preventDefault(),void this.finish(!0);if("Escape"===t.key&&this.resizeDrag)return t.preventDefault(),void this.finishResize(!0);const e=t.target.closest("[data-track-resize],[data-pane-resize]");if(e&&["ArrowUp","ArrowDown","Home"].includes(t.key)){t.preventDefault();const i="ArrowUp"===t.key?-1:1;return void(e.dataset.trackResize?(this._tracks=this._tracks.map(s=>s.id===e.dataset.trackResize?{...s,height:"Home"===t.key?this.rowHeight:a(this.trackHeight(s)+i*(t.shiftKey?8:2),32,120)}:s),this.render(),d(this,"db-track-change",{tracks:this.tracks}),this.querySelector(`[data-track-resize="${CSS.escape(e.dataset.trackResize)}"]`)?.focus()):(this.height="Home"===t.key?0:a((this.height||this.viewport.clientHeight)+i*(t.shiftKey?24:8),120,720),d(this,"db-resize",{height:this.height}),this.paneGrip?.focus()))}if(this.shortcuts&&!t.altKey&&["+","=","-","0"].includes(t.key))return t.preventDefault(),t.stopPropagation(),void("0"===t.key?this.changeZoom((this.viewport.clientWidth-this.labelWidth)/this.duration/.8,void 0,"fit"):this.changeZoom(this.zoom*("-"===t.key?1/1.2:1.2)));if(this.disabled||n(t.target)||t.ctrlKey||t.metaKey||t.altKey)return;const i=t.target.closest("[data-trim],[data-fade]");if(i&&["ArrowLeft","ArrowRight"].includes(t.key)){const e=i.closest("[data-clip]"),s=this._clips.find(t=>t.id===e?.dataset.clip);if(!s||this._tracks.find(t=>t.id===s.trackId)?.locked)return;t.preventDefault();const r="start"===i.dataset.trim?"trim-start":"end"===i.dataset.trim?"trim-end":"in"===i.dataset.fade?"fade-in":"fade-out";this.drag={kind:r,id:-1,clipId:s.id,originX:0,originY:0,scroll:this.viewport.scrollLeft,before:this.clips,x:("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps*this.scale,y:0,changed:!1,shift:!0,ripple:t.metaKey||t.ctrlKey},this.updateDrag(),this.finish();const a=i.dataset.focus;return void Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===a)?.focus({preventScroll:!0})}if(["ArrowLeft","ArrowRight","Home","End"].includes(t.key))t.preventDefault(),this.seek("Home"===t.key?0:"End"===t.key?this.duration:this.currentTime+("ArrowLeft"===t.key?-1:1)*(t.shiftKey?10:1)/this.fps),this.scrollToTime(this.currentTime);else if("s"===t.key.toLowerCase())t.preventDefault(),this.splitSelected();else if("Delete"===t.key||"Backspace"===t.key){const e=this._clips.find(t=>t.id===this.selectedClipId);if(!e||this._tracks.find(t=>t.id===e.trackId)?.locked)return;t.preventDefault(),this._clips=this._clips.filter(t=>t.id!==e.id),this.select(""),this.render(),d(this,"db-change",{clips:this.clips,clipId:e.id,action:"delete"})}else"Enter"===t.key&&t.target instanceof HTMLElement&&t.target.dataset.clip&&(t.preventDefault(),this.select(t.target.dataset.clip))}}static get observedAttributes(){return["duration","fps","current-time","zoom","snap","disabled","selected-clip-id","row-height","label-width","layer-gap","height","min-clip-duration","shortcuts"]}get layerGap(){return a(this.number("layer-gap",0),0,32)}set layerGap(t){this.setAttribute("layer-gap",String(t))}isSource(t){return"source"===t.scope||!t.scope&&"transform"===t.kind}rangeFinish(t=!1){const e=this.rangeDrag;if(!e)return;this.rangeDrag=void 0,this.effectEditing=!1;const i=JSON.stringify(this._clips)!==JSON.stringify(e.before);t&&(this._clips=e.before),this.hasPointerCapture(e.pointer)&&this.releasePointerCapture(e.pointer),i&&d(this,t?"db-input":"db-change",{clips:this.clips,clipId:e.clipId,effectId:e.effectId,action:"effect-range",...t?{cancelled:!0}:{}}),this.render()}renderRange(t,e,s,n){const o=this.node("div","db-vt__effect-range-lane");o.style.width=this.contentDuration*this.scale+"px";const h=this.node("div","db-vt__effect-range");h.dataset.rangeEffect=s.id,h.title=s.label,h.setAttribute("aria-label",s.label),h.append(this.node("span","db-vt__effect-range-name",s.label));const c=Math.max(0,s.start||0),l=Math.max(1/this.fps,s.duration??e.duration);h.style.left=(e.start+c)*this.scale+"px",h.style.width=`${Math.max(12,l*this.scale)}px`;for(const t of["start","end"]){const o=this.node("span","db-vt__effect-range-grip");o.dataset.rangeTrim=t,o.tabIndex=this.disabled||n.locked?-1:0,o.setAttribute("role","slider"),o.setAttribute("aria-label",`${i("start"===t?"media.trimStart":"media.trimEnd")} · ${s.label}`),o.setAttribute("aria-valuenow",String("start"===t?c:c+l)),o.setAttribute("aria-valuemin","0"),o.setAttribute("aria-valuemax",String(this.contentDuration)),o.addEventListener("keydown",i=>{if(this.disabled||n.locked||!["ArrowLeft","ArrowRight"].includes(i.key))return;i.preventDefault(),i.stopPropagation();const o=("ArrowLeft"===i.key?-1:1)*(i.shiftKey?10:1)/this.fps;let h=c,p=l;"start"===t?(h=a(h+o,0,h+p-1/this.fps),p=c+l-h):p=Math.max(1/this.fps,p+o),s.start=r(h,this.fps),s.duration=r(p,this.fps),d(this,"db-change",{clips:this.clips,clipId:e.id,effectId:s.id,action:"effect-range"}),this.render()}),h.append(o)}h.addEventListener("pointerdown",t=>{0!==t.button||this.disabled||n.locked||(t.preventDefault(),t.stopPropagation(),this.rangeDrag={pointer:t.pointerId,clipId:e.id,effectId:s.id,kind:t.target.dataset.rangeTrim||"move",x:t.clientX,scroll:this.viewport?.scrollLeft||0,before:this.clips},this.effectEditing=!0,this.setPointerCapture(t.pointerId))}),o.append(h),t.append(o)}get labelWidth(){return a(this.number("label-width",144),120,480)}set labelWidth(t){this.setAttribute("label-width",String(a(t,120,480)))}get contentDuration(){const t=Math.max(this.duration,...this._clips.flatMap(t=>[t.start+t.duration,...(t.effects||[]).filter(t=>!this.isSource(t)).map(e=>t.start+Math.max(0,e.start||0)+(e.duration??t.duration))]));return t>this.duration?t+Math.max(1,.15*this.duration):t}labelGrip(t){const e=this.node("span","db-vt__label-resize");e.tabIndex=this.disabled?-1:0,e.setAttribute("aria-disabled",String(this.disabled)),e.setAttribute("role","separator"),e.setAttribute("aria-orientation","vertical"),e.setAttribute("aria-label",i("media.resizeLabel")),e.setAttribute("aria-valuemin","120"),e.setAttribute("aria-valuemax","480"),e.setAttribute("aria-valuenow",String(this.labelWidth)),e.addEventListener("pointerdown",t=>{this.disabled||0!==t.button||(t.preventDefault(),t.stopPropagation(),this.labelDrag={id:t.pointerId,x:t.clientX,width:this.labelWidth},this.setPointerCapture(t.pointerId))}),e.addEventListener("keydown",t=>{this.disabled||["ArrowLeft","ArrowRight","Home"].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.labelWidth="Home"===t.key?144:this.labelWidth+("ArrowLeft"===t.key?-1:1)*(t.shiftKey?24:8),d(this,"db-label-resize",{width:this.labelWidth}))}),t.append(e)}get rowHeight(){return a(this.number("row-height",42),32,120)}set rowHeight(t){this.setAttribute("row-height",String(t))}get height(){const t=this.number("height",0);return t>0?a(t,120,720):0}set height(t){t>0?this.setAttribute("height",String(t)):this.removeAttribute("height")}get minClipDuration(){return Math.min(this.duration,Math.max(1/this.fps,Math.ceil(this.number("min-clip-duration",.5)*this.fps)/this.fps))}set minClipDuration(t){this.setAttribute("min-clip-duration",String(t))}get shortcuts(){return"false"!==this.getAttribute("shortcuts")}set shortcuts(t){this.setAttribute("shortcuts",String(t))}trackHeight(t){return a(o(t.height,this.rowHeight),32,120)}number(t,e){const i=this.getAttribute(t);return i?.trim()&&Number.isFinite(Number(i))?Number(i):e}get duration(){return a(this.number("duration",30),1/this.fps,86400)}set duration(t){this.setAttribute("duration",String(t))}get fps(){return a(Math.round(this.number("fps",30)),1,120)}set fps(t){this.setAttribute("fps",String(t))}get zoom(){return a(this.number("zoom",100),10,800)}set zoom(t){this.setAttribute("zoom",String(t))}get currentTime(){return r(a(this.number("current-time",0),0,this.duration),this.fps)}set currentTime(t){this.setAttribute("current-time",String(t))}get snap(){return"false"!==this.getAttribute("snap")}set snap(t){this.setAttribute("snap",String(t))}get disabled(){return this.hasAttribute("disabled")}set disabled(t){this.toggleAttribute("disabled",t)}get selectedClipId(){return this.getAttribute("selected-clip-id")||""}set selectedClipId(t){this.setAttribute("selected-clip-id",t)}get tracks(){return this._tracks.map(t=>({...t}))}set tracks(t){const e=new Set;this._tracks=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>({...t,label:String(t.label||t.id),kind:["video","audio","title","mixed"].includes(t.kind)?t.kind:"video"})),this.render()}get clips(){return this._clips.map(t=>({...t,...t.effects?{effects:t.effects.map(t=>structuredClone(t))}:{}}))}set clips(t){if(this.drag||this.effectEditing)return;const e=new Set;this._clips=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&!e.has(t.id)&&!!e.add(t.id)).map(t=>{const e=r(Math.max(0,o(t.start,0)),this.fps),i=Math.max(1/this.fps,r(o(t.duration,1),this.fps)),s=r(a(o(t.fadeIn,0),0,i),this.fps),d=r(a(o(t.fadeOut,0),0,Math.max(0,i-s)),this.fps);return{...t,label:String(t.label||t.id),start:e,duration:i,sourceOffset:Math.max(0,o(t.sourceOffset,0)),...void 0!==t.fadeIn?{fadeIn:s}:{},...void 0!==t.fadeOut?{fadeOut:d}:{},...t.effects?{effects:Array.isArray(t.effects)?t.effects.filter(t=>t&&"string"==typeof t.id).map(t=>({...t,label:String(t.label||t.id)})):[]}:{}}});for(const t of this._clips)if(!t.effects?.some(t=>this.isSource(t))){let e="source-transform";for(;t.effects?.some(t=>t.id===e);)e+="-source";const s=[["x","X",-1e4,1e4,0],["y","Y",-1e4,1e4,0],["size",i("media.sourceSize"),0,1e4,100],["rotation",i("media.sourceRotation"),-36e3,36e3,0]].map(([t,e,i,s,r])=>({id:String(t),label:String(e),min:Number(i),max:Number(s),keyframes:[{id:`${t}-initial`,time:0,value:Number(r)}]}));t.effects=[{id:e,label:i("media.sourceTransform"),kind:"transform",scope:"source",properties:s},...t.effects||[]]}JSON.stringify(this._clips)!==JSON.stringify(this.committedClips)&&(this.committedClips=this.clips,this.undoClips=[],this.redoClips=[]),this.render()}get markers(){return this._markers.map(t=>({...t}))}set markers(t){this._markers=(Array.isArray(t)?t:[]).filter(t=>t&&"string"==typeof t.id&&Number.isFinite(t.time)).map(t=>({...t,label:String(t.label),time:a(t.time,0,this.duration)})),this.render()}get scale(){return.8*this.zoom}connectedCallback(){if(this.classList.add("designbase-wc-video-timeline"),this.setAttribute("role","region"),!this.viewport){this.viewport=document.createElement("div"),this.viewport.className="db-vt__viewport",this.viewport.tabIndex=0,this.canvas=document.createElement("div"),this.canvas.className="db-vt__canvas",this.viewport.append(this.canvas),this.hint=document.createElement("div"),this.hint.className="db-vt__hint";const t=h(i("media.more"),"chevron-right","scroll");this.hint.append(t),this.paneGrip=document.createElement("div"),this.paneGrip.className="db-vt__pane-resize",this.paneGrip.dataset.paneResize="",this.paneGrip.tabIndex=0,this.paneGrip.setAttribute("role","separator"),this.paneGrip.setAttribute("aria-orientation","horizontal"),this.append(this.viewport,this.hint,this.paneGrip),this.viewport.addEventListener("scroll",()=>this.syncHint())}this.viewport.addEventListener("wheel",this.wheel,{passive:!1}),this.addEventListener("dblclick",this.doubleResize),this.addEventListener("db-change",this.recordTimelineHistory,!0),this.addEventListener("contextmenu",this.effectContextMenu),this.addEventListener("pointerdown",this.down),this.addEventListener("pointermove",this.move),this.addEventListener("pointerup",this.up),this.addEventListener("pointercancel",this.cancel),this.addEventListener("lostpointercapture",this.cancel),this.addEventListener("keydown",this.key),this.addEventListener("db-click",this.onActionClick),this.unsubscribe=s(()=>this.render()),this.resize=new ResizeObserver(()=>this.syncHint()),this.resize.observe(this),this.render()}disconnectedCallback(){if(this.labelDrag){const t=this.labelDrag;this.labelDrag=void 0,this.labelWidth=t.width}this.rollback(),this.resize?.disconnect(),this.unsubscribe?.(),this.viewport?.removeEventListener("wheel",this.wheel),this.removeEventListener("dblclick",this.doubleResize),this.finishResize(!0),this.removeEventListener("db-change",this.recordTimelineHistory,!0),this.removeEventListener("contextmenu",this.effectContextMenu),this.keyMenu?.remove(),this.keyMenu=void 0,this.removeEventListener("pointerdown",this.down),this.removeEventListener("pointermove",this.move),this.removeEventListener("pointerup",this.up),this.removeEventListener("pointercancel",this.cancel),this.removeEventListener("lostpointercapture",this.cancel),this.removeEventListener("keydown",this.key),this.removeEventListener("db-click",this.onActionClick)}attributeChangedCallback(t){"disabled"===t&&this.disabled&&(this.finishEffectDrag(!0),this.finish(!0),this.finishResize(!0)),"current-time"===t?this.syncCursor():this.render()}syncHint(){for(const t of this.querySelectorAll(".db-vt__selection-tools"))t.style.width=`${this.viewport?.clientWidth||240}px`;if(this.syncCursor(),this.viewport)for(const t of this.querySelectorAll("[data-marker]"))t.style.visibility=Number(t.dataset.marker)*this.scale<this.viewport.scrollLeft?"hidden":"";this.viewport&&this.hint&&(this.hint.hidden=this.viewport.scrollLeft+this.viewport.clientWidth>=this.viewport.scrollWidth-2)}syncCursor(){for(const t of this.querySelectorAll("[data-effect-property]")){const e=this._clips.find(e=>e.id===t.closest("[data-effect-clip]")?.dataset.effectClip),i=e?.effects?.find(e=>e.id===t.dataset.effectId)?.properties?.find(e=>e.id===t.dataset.effectProperty),s=t.querySelector(".db-vt__property-value");e&&i&&s&&(s.textContent=String(Math.round(100*c(i.keyframes,this.currentTime-e.start))/100))}if(!this.effectEditing)for(const t of this.querySelectorAll("db-keyframe-editor[data-effect-editor]")){const e=this._clips.find(e=>e.id===t.closest("[data-effect-clip]")?.dataset.effectClip);e&&(t.currentTime=a(this.currentTime-e.start,0,e.duration))}this.cursor&&(this.cursor.style.left=`${this.labelWidth+this.currentTime*this.scale}px`,this.cursor.style.visibility=this.currentTime*this.scale<(this.viewport?.scrollLeft||0)?"hidden":"",this.cursor.setAttribute("aria-valuenow",String(this.currentTime)),this.cursor.setAttribute("aria-valuetext",l(this.currentTime,this.fps)))}seek(t){this.currentTime=r(a(t,0,this.duration),this.fps),d(this,"db-seek",{time:this.currentTime})}scrollToTime(t){this.viewport&&(this.viewport.scrollLeft=Math.max(0,a(t,0,this.duration)*this.scale-(this.viewport.clientWidth-this.labelWidth)/2))}setZoom(t){const e=this.currentTime;this.zoom=t,this.scrollToTime(e)}node(t,e,i){const s=document.createElement(t);return s.className=e,void 0!==i&&(s.textContent=i),s}render(){if(this.effectEditing)return;const t=new Set(this._clips.flatMap(t=>(t.effects||[]).flatMap(e=>(e.properties||[]).flatMap(i=>i.keyframes.map(s=>this.effectToken(t.id,e.id,i.id,s.id))))));if(this.selectedEffectKeys=new Set(Array.from(this.selectedEffectKeys).filter(e=>t.has(e))),!this.canvas||!this.viewport)return;this.querySelector(":scope > .db-vt__readout")?.remove(),this.viewport.style.height=this.height?`${this.height}px`:"",this.viewport.style.maxHeight=this.height?"720px":"360px",this.paneGrip&&(this.paneGrip.setAttribute("aria-label",i("media.resizeTimeline")),this.paneGrip.setAttribute("title",i("media.resizeTimelineHint")),this.paneGrip.setAttribute("aria-valuemin","120"),this.paneGrip.setAttribute("aria-valuemax","720"),this.paneGrip.setAttribute("aria-valuenow",String(this.height||this.viewport.clientHeight)));const e=this.hint?.querySelector("db-button");e?.setAttribute("aria-label",i("media.more")),e?.setAttribute("title",i("media.more"));const s=this.ownerDocument.activeElement?.dataset.focus;this.style.setProperty("--db-vt-layer-gap",`${this._tracks.length>1?this.layerGap:0}px`),this.style.setProperty("--db-vt-label-width",`${this.labelWidth}px`);const r=document.createDocumentFragment(),d=this.node("div","db-vt__ruler"),n=this.node("span","db-vt__corner",i("media.tracks"));this.labelGrip(n),d.append(n);const o=[1/this.fps,.1,.25,.5,1,2,5,10,30,60,300,600,3600].find(t=>t*this.scale>=64&&this.duration/t<=200)||3600;for(let t=0;t<=this.contentDuration;t+=o){const e=this.node("span","db-vt__tick",l(t,this.fps));e.style.left=`${this.labelWidth+t*this.scale}px`,d.append(e)}d.dataset.seek="",r.append(d),this._tracks.forEach((t,e)=>{const s=this.node("div","db-vt__row");s.dataset.track=t.id,s.dataset.layerIndex=String(e),s.dataset.kind=t.kind,s.style.setProperty("--db-vt-track-height",`${this.trackHeight(t)}px`),s.dataset.condensed=String(this.trackHeight(t)<44),s.dataset.locked=String(!!t.locked),s.dataset.muted=String(!!t.muted);const d=this.node("div","db-vt__track"),n=this._clips.filter(e=>e.trackId===t.id),c=n.length>0,p=document.createElement("db-button");if(p.setAttribute("size","s"),p.setAttribute("variant","ghost"),p.className="db-vt__layer-toggle",p.dataset.action="layer-disclosure",p.title=t.label,p.setAttribute("aria-label",t.label),p.append(this.node("span","db-vt__track-name",t.label)),c){const t=n.some(t=>this.expandedEffects.has(t.id));p.setAttribute("start-icon",t?"chevron-down":"chevron-right"),p.setAttribute("aria-expanded",String(t)),p.addEventListener("db-click",e=>{e.stopPropagation();for(const e of n)t?this.expandedEffects.delete(e.id):this.expandedEffects.add(e.id);this.render()})}d.append(p);for(const e of["lock","mute"]){const s=i("lock"===e?t.locked?"media.trackLocked":"media.trackUnlocked":t.muted?"media.soundOff":"media.soundOn"),r=h(`${s} · ${t.label}`,"lock"===e?t.locked?"lock":"unlock":t.muted?"mute":"volume-up",e);r.setAttribute("variant","ghost"),r.dataset.track=t.id,r.setAttribute("aria-pressed",String("lock"===e?!!t.locked:!!t.muted)),r.toggleAttribute("disabled",this.disabled),d.append(r)}this.labelGrip(d),s.append(d);const f=this.node("div","db-vt__lane");f.dataset.seek="",f.style.width=this.contentDuration*this.scale+"px",f.style.setProperty("--tick",o*this.scale+"px"),this._clips.filter(e=>e.trackId===t.id).forEach(e=>{const s=this.node("div","db-vt__clip");s.dataset.clip=e.id,s.dataset.focus=e.id,s.dataset.selected=String(this.selectedClipId===e.id),s.setAttribute("role","group"),s.tabIndex=0,s.setAttribute("aria-label",`${e.label}, ${l(e.start,this.fps)}, ${i("media.duration")} ${l(e.duration,this.fps)}`),s.setAttribute("aria-disabled",String(this.disabled||!!t.locked));const r=this.selectedClipId===e.id;if(s.dataset.appearance=e.appearance||("audio"===(e.kind||t.kind)?"waveform":"standard"),s.dataset.narrow=String(e.duration*this.scale<100),s.dataset.editing=String(this.drag?.clipId===e.id),s.style.left=e.start*this.scale+"px",e.start+e.duration>this.duration){const t=Math.max(0,this.duration-e.start)*this.scale;s.style.maskImage=`linear-gradient(to right, black ${t}px, rgba(0,0,0,.35) ${t}px)`,s.dataset.outOfRange="true"}s.style.width=`${Math.max(24,e.duration*this.scale)}px`,s.dataset.hitProxy=String(e.duration*this.scale<24),s.style.setProperty("--db-vt-actual-width",e.duration*this.scale+"px");for(const a of["start","end"]){const d=this.node("span","db-vt__trim");d.dataset.trim=a,d.dataset.focus=`${e.id}-trim-${a}`,d.tabIndex=!r||this.disabled||t.locked?-1:0,d.setAttribute("role","slider"),d.setAttribute("aria-label",`${i("start"===a?"media.trimStart":"media.trimEnd")} · ${e.label}`),d.setAttribute("aria-valuemin","0"),d.setAttribute("aria-valuemax",String(this.duration)),d.setAttribute("aria-valuenow",String("start"===a?e.start:e.start+e.duration)),d.setAttribute("aria-valuetext",l("start"===a?e.start:e.start+e.duration,this.fps)),d.setAttribute("title",i("start"===a?"media.trimStart":"media.trimEnd")),d.append(this.node("i","db-vt__grip")),s.append(d)}if("filmstrip"===e.appearance&&e.thumbnail){const t=this.node("div","db-vt__filmstrip");t.setAttribute("aria-hidden","true");for(let i=0;i<Math.min(24,Math.ceil(e.duration*this.scale/64));i++){const i=document.createElement("img");i.src=e.thumbnail,i.alt="",i.draggable=!1,t.append(i)}s.append(t)}for(const d of["in","out"]){const n=a("in"===d?e.fadeIn||0:e.fadeOut||0,0,e.duration),o=this.node("span","db-vt__fade");if(o.dataset.fadeArea=d,o.style.width=n*this.scale+"px",o.setAttribute("aria-hidden","true"),s.append(o),r){const r=this.node("span","db-vt__fade-handle");r.dataset.fade=d,r.dataset.focus=`${e.id}-fade-${d}`,r.style["in"===d?"left":"right"]=n*this.scale+"px",r.tabIndex=this.disabled||t.locked?-1:0,r.setAttribute("role","slider"),r.setAttribute("aria-label",`${i("in"===d?"media.fadeIn":"media.fadeOut")} · ${e.label}`),r.setAttribute("aria-valuemin","0"),r.setAttribute("aria-valuemax",String(e.duration)),r.setAttribute("aria-valuenow",String(n)),r.setAttribute("title",i("in"===d?"media.fadeIn":"media.fadeOut")),s.append(r)}}if(s.append(this.node("span","db-vt__clip-label",e.label)),"audio"===(e.kind||t.kind)||"waveform"===e.appearance){const t=this.node("span","db-vt__wave");t.setAttribute("aria-hidden","true");for(let i=0;i<Math.min(160,Math.ceil(e.duration*this.scale/7));i++){const e=this.node("i","");e.style.height=20+75*Math.abs(Math.sin(1.7*i)*Math.cos(.4*i))+"%",t.append(e)}s.append(t)}if(e.effects?.length){const t=this.node("span","db-vt__effects");t.setAttribute("aria-label",i("media.effects"));for(const i of e.effects){const e=this.node("span","db-vt__effect",i.label);e.dataset.enabled=String(!1!==i.enabled),e.dataset.kind=i.kind,e.setAttribute("title",i.label),t.append(e)}s.append(t)}f.append(s)}),s.append(f);const u=this.node("div","db-vt__track-resize");u.dataset.trackResize=t.id,u.tabIndex=0,u.setAttribute("role","separator"),u.setAttribute("aria-orientation","horizontal"),u.setAttribute("aria-label",`${i("media.resizeTrack")} · ${t.label}`),u.setAttribute("aria-valuemin","32"),u.setAttribute("aria-valuemax","120"),u.setAttribute("aria-valuenow",String(this.trackHeight(t))),s.append(u),r.append(s),this.renderEffects(r,t)});for(const t of this._markers){const e=this.node("button","db-vt__marker");e.type="button",e.dataset.marker=String(t.time),e.style.left=`${this.labelWidth+t.time*this.scale}px`;const s=`${i("media.marker")} · ${t.label} · ${l(t.time,this.fps)}`;e.setAttribute("aria-label",s),e.setAttribute("title",s),r.append(e)}if(this.cursor=this.node("div","db-vt__playhead"),this.cursor.dataset.playhead="",this.cursor.tabIndex=0,this.cursor.setAttribute("role","slider"),this.cursor.dataset.focus="playhead",this.cursor.setAttribute("aria-label",i("media.playhead")),this.cursor.setAttribute("aria-valuemin","0"),this.cursor.setAttribute("aria-valuemax",String(this.duration)),this.cursor.setAttribute("aria-disabled",String(this.disabled)),r.append(this.cursor),this._tracks.length||r.append(this.node("p","db-vt__empty",i("media.emptyTracks"))),this.canvas.style.width=`${this.labelWidth+this.contentDuration*this.scale}px`,this.drag?.clipId){const t=this._clips.find(t=>t.id===this.drag?.clipId);if(t){const e="fade-in"===this.drag.kind?t.fadeIn||0:"fade-out"===this.drag.kind?t.fadeOut||0:"trim-end"===this.drag.kind?t.start+t.duration:t.start,s=this.node("div","db-vt__readout",`${i("media."+("trim-start"===this.drag.kind?"trimStart":"trim-end"===this.drag.kind?"trimEnd":"fade-in"===this.drag.kind?"fadeIn":"fade-out"===this.drag.kind?"fadeOut":"time"))} ${l(e,this.fps)} / ${i("media.duration")} ${l(t.duration,this.fps)}`);this.append(s)}}this.canvas.replaceChildren(r),this.viewport.setAttribute("aria-label",i("media.timeline")),this.syncCursor(),this.syncHint(),s&&!this.drag&&Array.from(this.querySelectorAll("[data-focus]")).find(t=>t.dataset.focus===s)?.focus({preventScroll:!0})}renderEffects(t,i){for(const s of this._clips.filter(t=>t.trackId===i.id&&t.effects?.length)){if(!this.expandedEffects.has(s.id))continue;const r=this.node("section","db-vt__effect-tree");r.dataset.effectClip=s.id;const toggle=(t,e,i,s)=>{const r=h(t,i?"chevron-down":"chevron-right","effect-disclosure");return r.removeAttribute("icon-only"),r.setAttribute("variant","ghost"),r.textContent=t,r.setAttribute("aria-expanded",String(i)),r.setAttribute("aria-controls",e),r.dataset.focus=e,r.addEventListener("db-click",t=>{t.stopPropagation(),s(),this.render()}),r},n=`${this.effectTreeId}-${s.id}`,o=this.node("div","db-vt__effect-content");if(o.id=n,o.hidden=!this.expandedEffects.has(s.id),!o.hidden)for(const t of[...s.effects||[]].sort((t,e)=>Number(this.isSource(e))-Number(this.isSource(t)))){const r=this.isSource(t),h=`${s.id}/${t.id}`,p=`${n}-${t.id}`;if(!r){const e=this.node("div","db-vt__effect-range-row");e.dataset.effectId=t.id;const r=this.node("div","db-vt__effect-range-label");r.append(toggle(t.label,p,this.expandedEffects.has(h),()=>{this.expandedEffects.has(h)?this.expandedEffects.delete(h):this.expandedEffects.add(h)})),this.labelGrip(r),e.append(r),this.renderRange(e,s,t,i),o.append(e)}const f=this.node("div","db-vt__property-group");if(f.id=p,f.dataset.scope=r?"source":"effect",f.hidden=!r&&!this.expandedEffects.has(h),!f.hidden)for(const r of t.properties||[]){const n=`${h}/${r.id}`,o=this.node("div","db-vt__property-row");o.dataset.effectProperty=r.id,o.dataset.effectId=t.id;const p=this.node("div","db-vt__property-label"),u=document.createElement("db-button");u.setAttribute("size","s"),u.setAttribute("variant","ghost"),u.dataset.action="effect-property",u.setAttribute("aria-label",r.label),u.append(this.node("span","db-vt__property-name",r.label)),u.setAttribute("aria-expanded",String(this.activeProperty.get(s.id)===n)),u.dataset.focus=n,u.addEventListener("db-click",t=>{t.stopPropagation(),this.activeProperty.set(s.id,this.activeProperty.get(s.id)===n?"":n),this.render()}),u.title=r.label,p.append(u,this.node("span","db-vt__property-value",String(Math.round(100*c(r.keyframes,this.currentTime-s.start))/100))),this.labelGrip(p),o.append(p);const m=this.node("div","db-vt__property-lane");m.style.width=this.contentDuration*this.scale+"px";for(const e of r.keyframes){const a=this.node("button","db-vt__property-key");a.dataset.propertyKey=e.id,a.dataset.keyToken=this.effectToken(s.id,t.id,r.id,e.id),a.setAttribute("aria-pressed",String(this.selectedEffectKeys.has(a.dataset.keyToken))),a.addEventListener("pointerdown",i=>{if(a.disabled||0!==i.button)return;i.preventDefault(),i.stopPropagation(),this.viewport?.focus({preventScroll:!0});const d=a.dataset.keyToken;Array.from(this.selectedEffectKeys).some(t=>JSON.parse(t)[0]!==s.id)&&this.selectedEffectKeys.clear(),i.shiftKey?this.selectedEffectKeys.has(d)?this.selectedEffectKeys.delete(d):this.selectedEffectKeys.add(d):this.selectedEffectKeys.has(d)||(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(d)),this.syncEffectSelection(),this.selectedEffectKeys.has(d)?(this.effectDrag={pointer:i.pointerId,clipId:s.id,effectId:t.id,propertyId:r.id,keyId:e.id,x:i.clientX,time:e.time,before:this.clips,changed:!1,copies:i.altKey?this.makeKeyCopyIds():void 0},this.effectEditing=!0,this.setPointerCapture(i.pointerId)):this.render()}),a.type="button",a.style.left=(s.start+e.time)*this.scale+"px",a.setAttribute("aria-label",`${r.label} · ${l(e.time,this.fps)} · ${e.value}`),a.disabled=this.disabled||!!i.locked||!1===t.enabled,a.addEventListener("click",t=>{t.stopPropagation(),t.shiftKey||(this.selectedEffectKeys.has(a.dataset.keyToken)||(this.selectedEffectKeys.clear(),this.selectedEffectKeys.add(a.dataset.keyToken)),this.activeProperty.set(s.id,n),this.effectKeySelection.set(n,e.id),this.seek(s.start+e.time),this.render())}),m.append(a)}if(o.append(m),f.append(o),this.activeProperty.get(s.id)===n){const o=new e;o.dataset.effectEditor=r.id,o.duration=s.duration,o.fps=this.fps,o.min=r.min,o.max=r.max,o.setAttribute("label",r.label),o.keyframes=r.keyframes,o.selectedKeyframeId=this.effectKeySelection.get(n)||r.keyframes[0]?.id||"",o.currentTime=a(this.currentTime-s.start,0,s.duration),o.disabled=this.disabled||!!i.locked||!1===t.enabled;const edit=e=>{e.stopPropagation();const i=e.detail;this.effectEditing="db-input"===e.type&&!i.cancelled;const a=i.keyframes;this._clips=this._clips.map(e=>e.id!==s.id?e:{...e,effects:e.effects?.map(e=>e.id!==t.id?e:{...e,properties:e.properties?.map(t=>t.id!==r.id?t:{...t,keyframes:a})})}),d(this,e.type,{clips:this.clips,clipId:s.id,action:"keyframe",effectId:t.id,propertyId:r.id,...i.cancelled?{cancelled:!0}:{}}),("db-change"===e.type||i.cancelled)&&(this.effectEditing=!1,this.render())};o.addEventListener("db-input",edit),o.addEventListener("db-change",edit),o.addEventListener("db-selection-change",t=>{t.stopPropagation(),this.effectKeySelection.set(n,t.detail.keyframeId)}),o.addEventListener("db-seek",t=>{t.stopPropagation(),this.seek(s.start+t.detail.time)});const h=this.node("div","db-vt__effect-editor");h.style.width=`${Math.max(240,(this.viewport?.clientWidth||800)-16)}px`,h.append(o),f.append(h)}}o.append(f)}r.addEventListener("pointerdown",t=>{if(0!==t.button||this.disabled||i.locked||t.target.closest("db-button,db-keyframe-editor,db-input,db-select,.db-vt__property-label,[data-property-key]"))return;t.preventDefault(),t.stopPropagation(),this.viewport?.focus({preventScroll:!0});const e=this.node("div","db-vt__marquee");this.append(e),this.marquee={pointer:t.pointerId,x:t.clientX,y:t.clientY,clipId:s.id,before:t.shiftKey?new Set(Array.from(this.selectedEffectKeys).filter(t=>JSON.parse(t)[0]===s.id)):new Set,box:e},t.shiftKey||this.selectedEffectKeys.clear(),this.syncEffectSelection(),this.setPointerCapture(t.pointerId)}),r.append(o),t.append(r)}}select(t){this.selectedClipId!==t&&(this.selectedClipId=t,d(this,"db-selection-change",{clipId:t}))}timeAt(t){return(t-this.viewport.getBoundingClientRect().left+this.viewport.scrollLeft-this.labelWidth)/this.scale}snapped(t,e){let i=r(t,this.fps);if(this.snap&&!this.drag?.shift){const t=[0,this.currentTime,...this._markers.map(t=>t.time),...this._clips.filter(t=>t.id!==e).flatMap(t=>[t.start,t.start+t.duration])].reduce((t,e)=>Math.abs(e-i)<Math.abs(t-i)?e:t,1/0);Math.abs(t-i)*this.scale<=8&&(i=r(t,this.fps))}return i}updateDrag(){const t=this.drag;if(!t)return;if("seek"===t.kind)return void this.seek(this.timeAt(t.x));const e=t.before.find(e=>e.id===t.clipId);if(!e)return;const i=(t.x-t.originX+this.viewport.scrollLeft-t.scroll)/this.scale;let s=e.start,n=e.duration,o=e.sourceOffset||0,h=e.trackId;if("fade-in"===t.kind||"fade-out"===t.kind){const s="fade-in"===t.kind?"fadeIn":"fadeOut",n="fade-in"===t.kind?e.fadeOut||0:e.fadeIn||0,o=r(a((e[s]||0)+("fade-in"===t.kind?i:-i),0,Math.max(0,e.duration-n)),this.fps);return this._clips=t.before.map(t=>t.id===e.id?{...t,[s]:o}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),void d(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}if("move"===t.kind){s=this.snapped(s+i,e.id),this.canvas.getBoundingClientRect();const r=this.trackIndexAt(t.y),d=this._tracks[a(r,0,this._tracks.length-1)],n=this._tracks.find(t=>t.id===e.trackId);!d||d.locked||"mixed"!==d.kind&&d.kind!==(e.kind||n?.kind)||(h=d.id),s=Math.max(0,s)}else if("trim-start"===t.kind)s=a(this.snapped(s+i,e.id),Math.max(0,e.start-o),e.start+n-Math.min(this.minClipDuration,e.start+n-Math.max(0,e.start-o))),n=e.start+e.duration-s,o+=s-e.start;else{const t=e.sourceDuration?Math.min(this.duration-s,e.sourceDuration-o):this.duration-s;n=a(this.snapped(s+n+i,e.id)-s,Math.min(this.minClipDuration,t),t)}const c=t.ripple&&("trim-start"===t.kind||"trim-end"===t.kind),l=t.before.filter(t=>t.id!==e.id&&t.trackId===e.trackId&&t.start>=e.start+e.duration-1e-6);let p=0;if(c){"trim-start"===t.kind&&(s=e.start);const i=Math.max(e.start+e.duration,...l.map(t=>t.start+t.duration));n=Math.min(n,e.duration+this.duration-i),p=r(n-e.duration,this.fps)}let f=t.before.map(t=>l.some(e=>e.id===t.id)&&c?{...t,start:r(t.start+p,this.fps)}:t);if("move"===t.kind&&t.ripple){const i=t.before.filter(i=>i.trackId===h&&(t.duplicate||i.id!==e.id)).sort((t,e)=>t.start-e.start),a=i.find(t=>s>t.start+1e-6&&s<t.start+t.duration-1e-6);a&&(s=a.start);let d=s+n;const o=new Map;for(const t of i){if(t.start<s-1e-6)continue;const e=Math.max(t.start,d);o.set(t.id,e),d=e+t.duration}f=t.before.map(t=>o.has(t.id)?{...t,start:r(o.get(t.id),this.fps)}:t)}if(f.some(i=>(t.duplicate||i.id!==e.id)&&i.trackId===h&&s<i.start+i.duration-1e-6&&s+n>i.start+1e-6))return;const u=t.duplicate||e.id;t.duplicate&&(f=[...f,{...e,id:u}]),this._clips=f.map(t=>t.id===u?{...t,kind:t.kind||("mixed"===this._tracks.find(t=>t.id===e.trackId)?.kind?"video":this._tracks.find(t=>t.id===e.trackId)?.kind),start:r(s,this.fps),duration:r(n,this.fps),sourceOffset:r(o,this.fps),trackId:h,...void 0!==t.fadeIn?{fadeIn:Math.min(t.fadeIn,n)}:{},...void 0!==t.fadeOut?{fadeOut:Math.min(t.fadeOut,Math.max(0,n-(t.fadeIn||0)))}:{}}:t),t.changed=JSON.stringify(this._clips)!==JSON.stringify(t.before),this.render(),d(this,"db-input",{clips:this.clips,clipId:e.id,action:t.kind})}restoreTimelineHistory(t=!1){const e=t?this.redoClips:this.undoClips,i=t?this.undoClips:this.redoClips,s=e.pop();s&&(i.push(this.clips),this._clips=structuredClone(s),this.committedClips=this.clips,this.selectedEffectKeys.clear(),this.render(),d(this,"db-change",{clips:this.clips,clipId:this.selectedClipId,action:t?"redo":"undo"}))}makeKeyCopyIds(){return new Map(Array.from(this.selectedEffectKeys,t=>[t,`key-${crypto.randomUUID()}`]))}copyKeyData(t,e,i,s){let a=!0,d=0;const n=t.map(t=>t.id!==e?t:{...t,effects:t.effects?.map(e=>({...e,properties:e.properties?.map(n=>{const o=n.keyframes.filter(i=>s.has(this.effectToken(t.id,e.id,n.id,i.id))).map(o=>{const h=r(o.time+i,this.fps);return d++,(h<0||h>t.duration||n.keyframes.some(t=>Math.abs(t.time-h)<.5/this.fps))&&(a=!1),{...structuredClone(o),id:s.get(this.effectToken(t.id,e.id,n.id,o.id)),time:h}});return{...n,keyframes:[...n.keyframes,...o].sort((t,e)=>t.time-e.time)}})}))});return a&&d?n:void 0}duplicateSelectedKeys(t){const e=this.makeKeyCopyIds(),i=this.clips;for(let s=1;s<=Math.min(3e3,Math.ceil(this.duration*this.fps));s++)for(const r of[1,-1]){const a=this.copyKeyData(i,t,r*s/this.fps,e);if(a)return this._clips=a,this.selectedEffectKeys=new Set(Array.from(e,([t,e])=>{const[i,s,r]=JSON.parse(t);return this.effectToken(i,s,r,e)})),d(this,"db-change",{clips:this.clips,clipId:t,action:"keyframe"}),void this.render()}}effectToken(t,e,i,s){return JSON.stringify([t,e,i,s])}syncEffectSelection(){for(const t of this.querySelectorAll("[data-key-token]"))t.setAttribute("aria-pressed",String(this.selectedEffectKeys.has(t.dataset.keyToken)))}finishMarquee(t=!1){const e=this.marquee;e&&(this.marquee=void 0,e.box.remove(),t&&(this.selectedEffectKeys=e.before),this.hasPointerCapture(e.pointer)&&this.releasePointerCapture(e.pointer),this.render())}editSelectedKeys(t,e){const i=this._clips.find(e=>e.id===t);!i||this.disabled||this._tracks.find(t=>t.id===i.trackId)?.locked||(this._clips=this._clips.map(i=>i.id!==t?i:{...i,effects:i.effects?.map(t=>!1===t.enabled?t:{...t,properties:t.properties?.map(s=>{let r=s.keyframes;if(e)for(const a of s.keyframes)this.selectedEffectKeys.has(this.effectToken(i.id,t.id,s.id,a.id))&&(r=p(r,a.id,e));else r=r.filter(e=>!this.selectedEffectKeys.has(this.effectToken(i.id,t.id,s.id,e.id)));return{...s,keyframes:r}})})}),e||this.selectedEffectKeys.clear(),d(this,"db-change",{clips:this.clips,clipId:t,action:"keyframe"}),this.render(),this.viewport?.focus({preventScroll:!0}))}finishEffectDrag(t=!1){const e=this.effectDrag;e&&(this.effectDrag=void 0,this.effectEditing=!1,this.hasPointerCapture(e.pointer)&&this.releasePointerCapture(e.pointer),t?this._clips=e.before:e.copies&&e.changed&&(this.selectedEffectKeys=new Set(Array.from(e.copies,([t,e])=>{const[i,s,r]=JSON.parse(t);return this.effectToken(i,s,r,e)}))),(t||e.changed)&&d(this,t?"db-input":"db-change",{clips:this.clips,clipId:e.clipId,action:"keyframe",effectId:e.effectId,propertyId:e.propertyId,...t?{cancelled:!0}:{}}),this.render())}finish(t=!1){const e=this.drag;e&&(cancelAnimationFrame(this.raf),this.drag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._clips=e.before,this.render(),d(this,"db-input",{clips:this.clips,clipId:e.clipId,action:e.kind,cancelled:!0})):e.changed&&e.clipId&&"seek"!==e.kind&&d(this,"db-change",{clips:this.clips,clipId:e.clipId,action:e.kind}),this.render(),this.viewport?.focus({preventScroll:!0}))}rollback(){this.rangeFinish(!0),this.finishMarquee(!0),this.finishEffectDrag(!0),this.finish(!0)}splitSelected(){const t=this._clips.find(t=>t.id===this.selectedClipId);if(this.disabled||!t||this._tracks.find(e=>e.id===t.trackId)?.locked||t.duration<2*this.minClipDuration||this.currentTime<t.start+this.minClipDuration||this.currentTime>t.start+t.duration-this.minClipDuration)return;const e=this.currentTime;let i=t.id+"-split";for(;this._clips.some(t=>t.id===i);)i+="-1";this._clips=this._clips.flatMap(s=>s.id===t.id?[{...s,duration:e-s.start,fadeOut:0,fadeIn:Math.min(s.fadeIn||0,e-s.start)},{...s,id:i,fadeIn:0,fadeOut:Math.min(s.fadeOut||0,s.start+s.duration-e),start:e,duration:s.start+s.duration-e,sourceOffset:(s.sourceOffset||0)+e-s.start}]:[s]),this.render(),d(this,"db-change",{clips:this.clips,action:"split",clipId:t.id})}trackIndexAt(t){const e=Array.from(this.canvas.querySelectorAll(".db-vt__row"));let i=0;return e.forEach((e,s)=>{t>=e.getBoundingClientRect().top&&(i=s)}),i}finishResize(t=!1){const e=this.resizeDrag;e&&(this.resizeDrag=void 0,this.hasPointerCapture(e.id)&&this.releasePointerCapture(e.id),t?(this._tracks=e.beforeTracks,this.height=e.beforeHeight,this.render(),d(this,e.trackId?"db-track-input":"db-resize-input",e.trackId?{tracks:this.tracks,cancelled:!0}:{height:this.height,cancelled:!0})):d(this,e.trackId?"db-track-change":"db-resize",e.trackId?{tracks:this.tracks}:{height:this.height}))}changeZoom(t,e,i="keyboard"){if(!this.viewport)return;const s=this.zoom,r=e??Math.min(this.viewport.clientWidth-8,Math.max(152,this.labelWidth+this.currentTime*this.scale-this.viewport.scrollLeft)),n=(this.viewport.scrollLeft+r-this.labelWidth)/this.scale;this.zoom=Math.round(100*a(t,10,800))/100,this.viewport.scrollLeft="fit"===i?0:Math.max(0,this.labelWidth+n*this.scale-r),this.zoom!==s&&d(this,"db-zoom-change",{value:this.zoom,previousValue:s,anchorTime:n,source:i})}}customElements.get("db-video-timeline")||customElements.define("db-video-timeline",DbVideoTimeline);export{DbVideoTimeline};
|
|
@@ -18,6 +18,11 @@ interface VideoEffectProperty {
|
|
|
18
18
|
keyframes: EditorKeyframe[];
|
|
19
19
|
}
|
|
20
20
|
interface VideoClipEffect {
|
|
21
|
+
/** Source transform channels or an applied effect. Legacy transform groups are source channels. */
|
|
22
|
+
scope?: 'source' | 'effect';
|
|
23
|
+
/** Effect range in clip-local seconds; defaults to the complete clip. Keys remain clip-local. */
|
|
24
|
+
start?: number;
|
|
25
|
+
duration?: number;
|
|
21
26
|
properties?: VideoEffectProperty[];
|
|
22
27
|
id: string;
|
|
23
28
|
label: string;
|