@hyperframes/studio 0.4.37 → 0.4.38
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/dist/assets/index-18P_dZeo.js +93 -0
- package/dist/assets/index-BLrgRQSu.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/App.tsx +130 -51
- package/src/components/nle/NLELayout.tsx +43 -8
- package/src/components/sidebar/LeftSidebar.tsx +26 -0
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/PlayerControls.tsx +3 -44
- package/src/player/components/timelineTheme.ts +3 -3
- package/src/player/hooks/useTimelinePlayer.ts +2 -2
- package/src/player/lib/time.test.ts +11 -1
- package/src/player/lib/time.ts +6 -0
- package/src/utils/frameCapture.test.ts +26 -0
- package/src/utils/frameCapture.ts +38 -0
- package/dist/assets/index-Bj3m6A02.js +0 -93
- package/dist/assets/index-_h8opaGY.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.left-2{left:.5rem}.left-\[100px\]{left:100px}.left-\[176px\]{left:176px}.left-\[24px\]{left:24px}.left-\[31px\]{left:31px}.left-\[34px\]{left:34px}.left-\[52px\]{left:52px}.left-\[82px\]{left:82px}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.top-2{top:.5rem}.top-3{top:.75rem}.top-\[18px\]{top:18px}.top-\[21px\]{top:21px}.top-\[27px\]{top:27px}.top-\[3px\]{top:3px}.top-\[51px\]{top:51px}.top-full{top:100%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.z-\[2\]{z-index:2}.z-\[90\]{z-index:90}.z-\[91\]{z-index:91}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1{margin-left:.25rem}.ml-1\.5{margin-left:.375rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[18px\]{height:18px}.h-\[3px\]{height:3px}.h-\[45px\]{height:45px}.h-\[52px\]{height:52px}.h-\[5px\]{height:5px}.h-\[70px\]{height:70px}.h-full{height:100%}.h-px{height:1px}.max-h-24{max-height:6rem}.max-h-\[70\%\]{max-height:70%}.max-h-\[80vh\]{max-height:80vh}.max-h-full{max-height:100%}.min-h-0{min-height:0px}.min-h-7{min-height:1.75rem}.min-h-8{min-height:2rem}.min-h-9{min-height:2.25rem}.w-0{width:0px}.w-1\.5{width:.375rem}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-52{width:13rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[110px\]{width:110px}.w-\[118px\]{width:118px}.w-\[160px\]{width:160px}.w-\[320px\]{width:320px}.w-\[56px\]{width:56px}.w-\[58px\]{width:58px}.w-\[72px\]{width:72px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0px}.min-w-7{min-width:1.75rem}.min-w-8{min-width:2rem}.min-w-9{min-width:2.25rem}.min-w-\[140px\]{min-width:140px}.min-w-\[160px\]{min-width:160px}.min-w-\[56px\]{min-width:56px}.min-w-\[58px\]{min-width:58px}.min-w-\[96px\]{min-width:96px}.max-w-\[280px\]{max-width:280px}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-help{cursor:help}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize-y{resize:vertical}.\!resize{resize:both!important}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-1{row-gap:.25rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[10px\]{border-radius:10px}.rounded-\[11px\]{border-radius:11px}.rounded-\[14px\]{border-radius:14px}.rounded-\[9px\]{border-radius:9px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-green-500\/30{border-color:#22c55e4d}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-700\/40{border-color:#40404066}.border-neutral-700\/50{border-color:#40404080}.border-neutral-700\/60{border-color:#40404099}.border-neutral-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.border-neutral-800\/30{border-color:#2626264d}.border-neutral-800\/40{border-color:#26262666}.border-neutral-800\/50{border-color:#26262680}.border-neutral-800\/60{border-color:#26262699}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-700\/50{border-color:#b91c1c80}.border-studio-accent{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.border-studio-accent\/25{border-color:#3ce6ac40}.border-studio-accent\/30{border-color:#3ce6ac4d}.border-studio-accent\/50{border-color:#3ce6ac80}.border-studio-accent\/60{border-color:#3ce6ac99}.border-transparent{border-color:transparent}.border-white\/10{border-color:#ffffff1a}.border-white\/20{border-color:#fff3}.border-t-white{--tw-border-opacity: 1;border-top-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.bg-\[\#0a0a0b\]{--tw-bg-opacity: 1;background-color:rgb(10 10 11 / var(--tw-bg-opacity, 1))}.bg-\[\#0d1117\]{--tw-bg-opacity: 1;background-color:rgb(13 17 23 / var(--tw-bg-opacity, 1))}.bg-\[\#0f141c\]{--tw-bg-opacity: 1;background-color:rgb(15 20 28 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]\/10{background-color:#3ce6ac1a}.bg-\[\#3CE6AC\]\/5{background-color:#3ce6ac0d}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/80{background-color:#000c}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.bg-neutral-700\/40{background-color:#40404066}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.bg-neutral-800\/60{background-color:#26262699}.bg-neutral-800\/70{background-color:#262626b3}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.bg-neutral-900\/50{background-color:#17171780}.bg-neutral-900\/95{background-color:#171717f2}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/60{background-color:#7f1d1d99}.bg-red-900\/90{background-color:#7f1d1de6}.bg-red-950\/30{background-color:#450a0a4d}.bg-studio-accent{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.bg-studio-accent\/10{background-color:#3ce6ac1a}.bg-studio-accent\/15{background-color:#3ce6ac26}.bg-studio-accent\/20{background-color:#3ce6ac33}.bg-studio-accent\/\[0\.03\]{background-color:#3ce6ac08}.bg-studio-accent\/\[0\.05\]{background-color:#3ce6ac0d}.bg-studio-accent\/\[0\.06\]{background-color:#3ce6ac0f}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/5{background-color:#ffffff0d}.bg-white\/\[0\.035\]{background-color:#ffffff09}.bg-white\/\[0\.04\]{background-color:#ffffff0a}.bg-white\/\[0\.07\]{background-color:#ffffff12}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-3\.5{padding-top:.875rem;padding-bottom:.875rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0\.5{padding-bottom:.125rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-6{padding-left:1.5rem}.pr-1{padding-right:.25rem}.pr-9{padding-right:2.25rem}.pt-1{padding-top:.25rem}.pt-1\.5{padding-top:.375rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.16em\]{letter-spacing:.16em}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#09090B\]{--tw-text-opacity: 1;color:rgb(9 9 11 / var(--tw-text-opacity, 1))}.text-\[\#7f8796\]{--tw-text-opacity: 1;color:rgb(127 135 150 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-neutral-600{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.text-neutral-700{--tw-text-opacity: 1;color:rgb(64 64 64 / var(--tw-text-opacity, 1))}.text-neutral-950{--tw-text-opacity: 1;color:rgb(10 10 10 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-studio-accent{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.text-studio-accent\/50{color:#3ce6ac80}.text-studio-accent\/80{color:#3ce6accc}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/60{color:#fff9}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.accent-studio-accent{accent-color:#3CE6AC}.opacity-25{opacity:.25}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_18px_40px_rgba\(0\,0\,0\,0\.3\)\,0_4px_14px_rgba\(0\,0\,0\,0\.18\)\]{--tw-shadow: 0 18px 40px rgba(0,0,0,.3),0 4px 14px rgba(0,0,0,.18);--tw-shadow-colored: 0 18px 40px var(--tw-shadow-color), 0 4px 14px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.outline-1{outline-width:1px}.-outline-offset-1{outline-offset:-1px}.outline-\[\#3CE6AC\]\/30{outline-color:#3ce6ac4d}.outline-\[\#3CE6AC\]\/40{outline-color:#3ce6ac66}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-inset{--tw-ring-inset: inset}.ring-studio-accent{--tw-ring-opacity: 1;--tw-ring-color: rgb(60 230 172 / var(--tw-ring-opacity, 1))}.ring-white\/50{--tw-ring-color: rgb(255 255 255 / .5)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:root{color-scheme:dark}body{margin:0;padding:0;background:#0a0a0a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;overflow:hidden}#root{width:100vw;height:100vh;height:100dvh}.cm-editor{height:100%;font-size:13px}.cm-editor .cm-scroller{font-family:JetBrains Mono,Fira Code,SF Mono,monospace}.cm-editor.cm-focused{outline:none}.placeholder\:text-neutral-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.placeholder\:text-neutral-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-neutral-500:hover{--tw-border-opacity: 1;border-color:rgb(115 115 115 / var(--tw-border-opacity, 1))}.hover\:border-neutral-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.hover\:border-neutral-700:hover{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.hover\:border-neutral-800:hover{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.hover\:border-studio-accent\/50:hover{border-color:#3ce6ac80}.hover\:bg-neutral-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800:hover{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800\/30:hover{background-color:#2626264d}.hover\:bg-neutral-800\/50:hover{background-color:#26262680}.hover\:bg-neutral-900:hover{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-800\/60:hover{background-color:#991b1b99}.hover\:bg-red-900\/30:hover{background-color:#7f1d1d4d}.hover\:bg-studio-accent\/25:hover{background-color:#3ce6ac40}.hover\:bg-studio-accent\/80:hover{background-color:#3ce6accc}.hover\:bg-white\/\[0\.06\]:hover{background-color:#ffffff0f}.hover\:text-amber-300:hover{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.hover\:text-green-400:hover{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.hover\:text-neutral-100:hover{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.hover\:text-neutral-200:hover{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.hover\:text-neutral-300:hover{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.hover\:text-neutral-400:hover{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-studio-accent:hover{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:ring-1:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:ring-white\/30:hover{--tw-ring-color: rgb(255 255 255 / .3)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-\[\#3CE6AC\]:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-neutral-600:focus{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent\/40:focus{border-color:#3ce6ac66}.focus\:border-studio-accent\/60:focus{border-color:#3ce6ac99}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-studio-accent\/40:focus{--tw-ring-color: rgb(60 230 172 / .4)}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-studio-accent\/50:focus-visible{--tw-ring-color: rgb(60 230 172 / .5)}.active\:scale-\[0\.97\]:active{--tw-scale-x: .97;--tw-scale-y: .97;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:block{display:block}.group:hover .group-hover\:scale-125{--tw-scale-x: 1.25;--tw-scale-y: 1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media(min-width:640px){.sm\:inline{display:inline}.sm\:flex{display:flex}}@media(min-width:1024px){.lg\:flex{display:flex}}
|
package/dist/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
6
|
<title>HyperFrames Studio</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-18P_dZeo.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BLrgRQSu.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.38",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@phosphor-icons/react": "^2.1.10",
|
|
33
33
|
"codemirror": "^6.0.1",
|
|
34
34
|
"motion": "^12.38.0",
|
|
35
|
-
"@hyperframes/core": "0.4.
|
|
36
|
-
"@hyperframes/player": "0.4.
|
|
35
|
+
"@hyperframes/core": "0.4.38",
|
|
36
|
+
"@hyperframes/player": "0.4.38"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/react": "^19.0.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"vite": "^6.4.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"zustand": "^5.0.0",
|
|
50
|
-
"@hyperframes/producer": "0.4.
|
|
50
|
+
"@hyperframes/producer": "0.4.38"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/App.tsx
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useCallback,
|
|
4
|
+
useRef,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
type MouseEvent,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { useMountEffect } from "./hooks/useMountEffect";
|
|
3
11
|
import { NLELayout } from "./components/nle/NLELayout";
|
|
4
12
|
import { SourceEditor } from "./components/editor/SourceEditor";
|
|
5
13
|
import { LeftSidebar } from "./components/sidebar/LeftSidebar";
|
|
6
14
|
import { RenderQueue } from "./components/renders/RenderQueue";
|
|
7
15
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
8
|
-
import { CompositionThumbnail, VideoThumbnail, usePlayerStore } from "./player";
|
|
16
|
+
import { CompositionThumbnail, VideoThumbnail, liveTime, usePlayerStore } from "./player";
|
|
9
17
|
import { AudioWaveform } from "./player/components/AudioWaveform";
|
|
10
18
|
import type { TimelineElement } from "./player";
|
|
11
19
|
import { LintModal } from "./components/LintModal";
|
|
@@ -40,6 +48,8 @@ import {
|
|
|
40
48
|
getTimelineToggleTitle,
|
|
41
49
|
shouldHandleTimelineToggleHotkey,
|
|
42
50
|
} from "./utils/timelineDiscovery";
|
|
51
|
+
import { buildFrameCaptureFilename, buildFrameCaptureUrl } from "./utils/frameCapture";
|
|
52
|
+
import { Camera } from "./icons/SystemIcons";
|
|
43
53
|
|
|
44
54
|
interface EditingFile {
|
|
45
55
|
path: string;
|
|
@@ -264,6 +274,7 @@ export function StudioApp() {
|
|
|
264
274
|
const [globalDragOver, setGlobalDragOver] = useState(false);
|
|
265
275
|
const [appToast, setAppToast] = useState<AppToast | null>(null);
|
|
266
276
|
const [timelineVisible, setTimelineVisible] = useState(true);
|
|
277
|
+
const [captureFrameTime, setCaptureFrameTime] = useState(0);
|
|
267
278
|
const dragCounterRef = useRef(0);
|
|
268
279
|
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
269
280
|
const lastBlockedTimelineToastAtRef = useRef(0);
|
|
@@ -298,6 +309,26 @@ export function StudioApp() {
|
|
|
298
309
|
const toggleTimelineVisibility = useCallback(() => {
|
|
299
310
|
setTimelineVisible((visible) => !visible);
|
|
300
311
|
}, []);
|
|
312
|
+
const toggleLeftSidebar = useCallback(() => {
|
|
313
|
+
setLeftCollapsed((collapsed) => !collapsed);
|
|
314
|
+
}, []);
|
|
315
|
+
const refreshCaptureFrameTime = useCallback(() => {
|
|
316
|
+
setCaptureFrameTime(usePlayerStore.getState().currentTime);
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
useMountEffect(() => {
|
|
320
|
+
setCaptureFrameTime(usePlayerStore.getState().currentTime);
|
|
321
|
+
return liveTime.subscribe(setCaptureFrameTime);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const captureFrameHref = projectId
|
|
325
|
+
? buildFrameCaptureUrl({
|
|
326
|
+
projectId,
|
|
327
|
+
compositionPath: activeCompPath,
|
|
328
|
+
currentTime: captureFrameTime,
|
|
329
|
+
})
|
|
330
|
+
: "#";
|
|
331
|
+
const captureFrameFilename = buildFrameCaptureFilename(activeCompPath, captureFrameTime);
|
|
301
332
|
useMountEffect(() => () => {
|
|
302
333
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
303
334
|
});
|
|
@@ -496,6 +527,28 @@ export function StudioApp() {
|
|
|
496
527
|
>
|
|
497
528
|
+
|
|
498
529
|
</button>
|
|
530
|
+
<button
|
|
531
|
+
type="button"
|
|
532
|
+
onClick={toggleTimelineVisibility}
|
|
533
|
+
className="ml-1 flex h-7 w-7 items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-900 hover:text-neutral-200"
|
|
534
|
+
title={getTimelineToggleTitle(true)}
|
|
535
|
+
aria-label="Hide timeline editor"
|
|
536
|
+
>
|
|
537
|
+
<svg
|
|
538
|
+
width="14"
|
|
539
|
+
height="14"
|
|
540
|
+
viewBox="0 0 24 24"
|
|
541
|
+
fill="none"
|
|
542
|
+
stroke="currentColor"
|
|
543
|
+
strokeWidth="1.8"
|
|
544
|
+
strokeLinecap="round"
|
|
545
|
+
strokeLinejoin="round"
|
|
546
|
+
aria-hidden="true"
|
|
547
|
+
>
|
|
548
|
+
<path d="M5 7h14" />
|
|
549
|
+
<path d="m8 11 4 4 4-4" />
|
|
550
|
+
</svg>
|
|
551
|
+
</button>
|
|
499
552
|
</div>
|
|
500
553
|
</div>
|
|
501
554
|
</div>
|
|
@@ -787,6 +840,42 @@ export function StudioApp() {
|
|
|
787
840
|
toastTimerRef.current = setTimeout(() => setAppToast(null), 4000);
|
|
788
841
|
}, []);
|
|
789
842
|
|
|
843
|
+
const handleCaptureFrameClick = useCallback(
|
|
844
|
+
async (event: MouseEvent<HTMLAnchorElement>) => {
|
|
845
|
+
if (!projectId) return;
|
|
846
|
+
event.preventDefault();
|
|
847
|
+
|
|
848
|
+
const currentTime = usePlayerStore.getState().currentTime;
|
|
849
|
+
setCaptureFrameTime(currentTime);
|
|
850
|
+
const href = buildFrameCaptureUrl({
|
|
851
|
+
projectId,
|
|
852
|
+
compositionPath: activeCompPath,
|
|
853
|
+
currentTime,
|
|
854
|
+
});
|
|
855
|
+
const filename = buildFrameCaptureFilename(activeCompPath, currentTime);
|
|
856
|
+
|
|
857
|
+
try {
|
|
858
|
+
const response = await fetch(href, { cache: "no-store" });
|
|
859
|
+
if (!response.ok) {
|
|
860
|
+
throw new Error(`Capture failed (${response.status})`);
|
|
861
|
+
}
|
|
862
|
+
const blob = await response.blob();
|
|
863
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
864
|
+
const link = document.createElement("a");
|
|
865
|
+
link.href = blobUrl;
|
|
866
|
+
link.download = filename;
|
|
867
|
+
document.body.appendChild(link);
|
|
868
|
+
link.click();
|
|
869
|
+
link.remove();
|
|
870
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), 0);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
const message = err instanceof Error ? err.message : "Capture failed";
|
|
873
|
+
showToast(message);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
[activeCompPath, projectId, showToast],
|
|
877
|
+
);
|
|
878
|
+
|
|
790
879
|
const handleTimelineElementDelete = useCallback(
|
|
791
880
|
async (element: TimelineElement) => {
|
|
792
881
|
const pid = projectIdRef.current;
|
|
@@ -1345,55 +1434,19 @@ export function StudioApp() {
|
|
|
1345
1434
|
</div>
|
|
1346
1435
|
{/* Right: toolbar buttons */}
|
|
1347
1436
|
<div className="flex items-center gap-1.5">
|
|
1348
|
-
<
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
title=
|
|
1437
|
+
<a
|
|
1438
|
+
href={captureFrameHref}
|
|
1439
|
+
download={captureFrameFilename}
|
|
1440
|
+
onClick={handleCaptureFrameClick}
|
|
1441
|
+
onFocus={refreshCaptureFrameTime}
|
|
1442
|
+
onPointerDown={refreshCaptureFrameTime}
|
|
1443
|
+
className="h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border border-neutral-700 text-neutral-300 transition-colors hover:border-neutral-500 hover:bg-neutral-800"
|
|
1444
|
+
title="Capture current frame"
|
|
1445
|
+
aria-label="Capture current frame"
|
|
1356
1446
|
>
|
|
1357
|
-
<
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
viewBox="0 0 24 24"
|
|
1361
|
-
fill="none"
|
|
1362
|
-
stroke="currentColor"
|
|
1363
|
-
strokeWidth="1.5"
|
|
1364
|
-
strokeLinecap="round"
|
|
1365
|
-
strokeLinejoin="round"
|
|
1366
|
-
>
|
|
1367
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
1368
|
-
<path d="M9 3v18" />
|
|
1369
|
-
</svg>
|
|
1370
|
-
</button>
|
|
1371
|
-
<button
|
|
1372
|
-
type="button"
|
|
1373
|
-
onClick={toggleTimelineVisibility}
|
|
1374
|
-
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
|
|
1375
|
-
timelineVisible
|
|
1376
|
-
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
1377
|
-
: "text-neutral-300 border-neutral-700 hover:border-neutral-500 hover:bg-neutral-800"
|
|
1378
|
-
}`}
|
|
1379
|
-
title={getTimelineToggleTitle(timelineVisible)}
|
|
1380
|
-
aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
|
|
1381
|
-
>
|
|
1382
|
-
<svg
|
|
1383
|
-
width="14"
|
|
1384
|
-
height="14"
|
|
1385
|
-
viewBox="0 0 24 24"
|
|
1386
|
-
fill="none"
|
|
1387
|
-
stroke="currentColor"
|
|
1388
|
-
strokeWidth="1.5"
|
|
1389
|
-
strokeLinecap="round"
|
|
1390
|
-
>
|
|
1391
|
-
<rect x="3" y="13" width="18" height="8" rx="1" />
|
|
1392
|
-
<line x1="3" y1="9" x2="21" y2="9" />
|
|
1393
|
-
<line x1="3" y1="5" x2="21" y2="5" />
|
|
1394
|
-
</svg>
|
|
1395
|
-
<span>Timeline</span>
|
|
1396
|
-
</button>
|
|
1447
|
+
<Camera size={14} />
|
|
1448
|
+
<span>Capture</span>
|
|
1449
|
+
</a>
|
|
1397
1450
|
<button
|
|
1398
1451
|
onClick={() => setRightCollapsed((v) => !v)}
|
|
1399
1452
|
className={`h-7 flex items-center gap-1.5 px-2.5 rounded-md text-[11px] font-medium border transition-colors ${
|
|
@@ -1422,7 +1475,32 @@ export function StudioApp() {
|
|
|
1422
1475
|
{/* Main content: sidebar + preview + right panel */}
|
|
1423
1476
|
<div className="flex flex-1 min-h-0">
|
|
1424
1477
|
{/* Left sidebar: Compositions + Assets (resizable, collapsible) */}
|
|
1425
|
-
{
|
|
1478
|
+
{leftCollapsed ? (
|
|
1479
|
+
<div className="flex w-10 flex-shrink-0 flex-col items-center border-r border-neutral-800/50 bg-neutral-950 pt-1">
|
|
1480
|
+
<button
|
|
1481
|
+
type="button"
|
|
1482
|
+
onClick={toggleLeftSidebar}
|
|
1483
|
+
className="flex h-8 w-8 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
|
1484
|
+
title="Show sidebar"
|
|
1485
|
+
aria-label="Show sidebar"
|
|
1486
|
+
>
|
|
1487
|
+
<svg
|
|
1488
|
+
width="14"
|
|
1489
|
+
height="14"
|
|
1490
|
+
viewBox="0 0 24 24"
|
|
1491
|
+
fill="none"
|
|
1492
|
+
stroke="currentColor"
|
|
1493
|
+
strokeWidth="1.5"
|
|
1494
|
+
strokeLinecap="round"
|
|
1495
|
+
strokeLinejoin="round"
|
|
1496
|
+
aria-hidden="true"
|
|
1497
|
+
>
|
|
1498
|
+
<path d="M5 4v16" />
|
|
1499
|
+
<path d="m10 7 5 5-5 5" />
|
|
1500
|
+
</svg>
|
|
1501
|
+
</button>
|
|
1502
|
+
</div>
|
|
1503
|
+
) : (
|
|
1426
1504
|
<LeftSidebar
|
|
1427
1505
|
width={leftWidth}
|
|
1428
1506
|
projectId={projectId}
|
|
@@ -1469,6 +1547,7 @@ export function StudioApp() {
|
|
|
1469
1547
|
}
|
|
1470
1548
|
onLint={handleLint}
|
|
1471
1549
|
linting={linting}
|
|
1550
|
+
onToggleCollapse={toggleLeftSidebar}
|
|
1472
1551
|
/>
|
|
1473
1552
|
)}
|
|
1474
1553
|
|
|
@@ -5,6 +5,10 @@ import type { TimelineElement } from "../../player";
|
|
|
5
5
|
import type { BlockedTimelineEditIntent } from "../../player/components/timelineEditing";
|
|
6
6
|
import { NLEPreview } from "./NLEPreview";
|
|
7
7
|
import { CompositionBreadcrumb, type CompositionLevel } from "./CompositionBreadcrumb";
|
|
8
|
+
import {
|
|
9
|
+
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
10
|
+
getTimelineToggleTitle,
|
|
11
|
+
} from "../../utils/timelineDiscovery";
|
|
8
12
|
|
|
9
13
|
interface NLELayoutProps {
|
|
10
14
|
projectId: string;
|
|
@@ -197,6 +201,7 @@ export const NLELayout = memo(function NLELayout({
|
|
|
197
201
|
|
|
198
202
|
// Resizable timeline height
|
|
199
203
|
const [timelineH, setTimelineH] = useState(DEFAULT_TIMELINE_H);
|
|
204
|
+
const isTimelineVisible = timelineVisible ?? true;
|
|
200
205
|
const isDragging = useRef(false);
|
|
201
206
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
202
207
|
|
|
@@ -366,16 +371,11 @@ export const NLELayout = memo(function NLELayout({
|
|
|
366
371
|
onNavigate={handleNavigateComposition}
|
|
367
372
|
/>
|
|
368
373
|
)}
|
|
369
|
-
<PlayerControls
|
|
370
|
-
onTogglePlay={togglePlay}
|
|
371
|
-
onSeek={seek}
|
|
372
|
-
timelineVisible={timelineVisible ?? true}
|
|
373
|
-
onToggleTimeline={onToggleTimeline}
|
|
374
|
-
/>
|
|
374
|
+
<PlayerControls onTogglePlay={togglePlay} onSeek={seek} />
|
|
375
375
|
</div>
|
|
376
376
|
</div>
|
|
377
377
|
|
|
378
|
-
{
|
|
378
|
+
{isTimelineVisible ? (
|
|
379
379
|
<>
|
|
380
380
|
{/* Resize divider */}
|
|
381
381
|
<div
|
|
@@ -417,7 +417,42 @@ export const NLELayout = memo(function NLELayout({
|
|
|
417
417
|
{timelineFooter && <div className="flex-shrink-0">{timelineFooter}</div>}
|
|
418
418
|
</div>
|
|
419
419
|
</>
|
|
420
|
-
)
|
|
420
|
+
) : onToggleTimeline ? (
|
|
421
|
+
<div className="flex-shrink-0 border-t border-neutral-800/50 bg-neutral-950/96">
|
|
422
|
+
<div className="flex h-10 items-center justify-between px-3">
|
|
423
|
+
<div className="text-[10px] font-medium uppercase tracking-[0.16em] text-neutral-500">
|
|
424
|
+
Timeline
|
|
425
|
+
</div>
|
|
426
|
+
<button
|
|
427
|
+
type="button"
|
|
428
|
+
onClick={onToggleTimeline}
|
|
429
|
+
className="flex h-7 items-center gap-1.5 rounded-md border border-neutral-800 px-2.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-700 hover:bg-neutral-900 hover:text-neutral-100"
|
|
430
|
+
title={getTimelineToggleTitle(false)}
|
|
431
|
+
aria-label="Show timeline editor"
|
|
432
|
+
>
|
|
433
|
+
<svg
|
|
434
|
+
width="13"
|
|
435
|
+
height="13"
|
|
436
|
+
viewBox="0 0 24 24"
|
|
437
|
+
fill="none"
|
|
438
|
+
stroke="currentColor"
|
|
439
|
+
strokeWidth="1.7"
|
|
440
|
+
strokeLinecap="round"
|
|
441
|
+
strokeLinejoin="round"
|
|
442
|
+
aria-hidden="true"
|
|
443
|
+
>
|
|
444
|
+
<rect x="3" y="13" width="18" height="8" rx="1" />
|
|
445
|
+
<path d="M7 9h10" />
|
|
446
|
+
<path d="M8 5h8" />
|
|
447
|
+
</svg>
|
|
448
|
+
<span>Show</span>
|
|
449
|
+
<span className="hidden rounded bg-white/5 px-1 py-0.5 font-mono text-[9px] text-neutral-500 sm:inline">
|
|
450
|
+
{TIMELINE_TOGGLE_SHORTCUT_LABEL}
|
|
451
|
+
</span>
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
) : null}
|
|
421
456
|
</div>
|
|
422
457
|
);
|
|
423
458
|
});
|
|
@@ -35,6 +35,7 @@ interface LeftSidebarProps {
|
|
|
35
35
|
codeChildren?: ReactNode;
|
|
36
36
|
onLint?: () => void;
|
|
37
37
|
linting?: boolean;
|
|
38
|
+
onToggleCollapse?: () => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export const LeftSidebar = memo(function LeftSidebar({
|
|
@@ -57,6 +58,7 @@ export const LeftSidebar = memo(function LeftSidebar({
|
|
|
57
58
|
codeChildren,
|
|
58
59
|
onLint,
|
|
59
60
|
linting,
|
|
61
|
+
onToggleCollapse,
|
|
60
62
|
}: LeftSidebarProps) {
|
|
61
63
|
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
|
|
62
64
|
|
|
@@ -122,6 +124,30 @@ export const LeftSidebar = memo(function LeftSidebar({
|
|
|
122
124
|
>
|
|
123
125
|
Assets
|
|
124
126
|
</button>
|
|
127
|
+
{onToggleCollapse && (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={onToggleCollapse}
|
|
131
|
+
className="mx-1 my-1 flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-md border border-transparent text-neutral-500 transition-colors hover:border-neutral-800 hover:bg-neutral-900 hover:text-neutral-300"
|
|
132
|
+
title="Hide sidebar"
|
|
133
|
+
aria-label="Hide sidebar"
|
|
134
|
+
>
|
|
135
|
+
<svg
|
|
136
|
+
width="14"
|
|
137
|
+
height="14"
|
|
138
|
+
viewBox="0 0 24 24"
|
|
139
|
+
fill="none"
|
|
140
|
+
stroke="currentColor"
|
|
141
|
+
strokeWidth="1.5"
|
|
142
|
+
strokeLinecap="round"
|
|
143
|
+
strokeLinejoin="round"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
>
|
|
146
|
+
<path d="m14 7-5 5 5 5" />
|
|
147
|
+
<path d="M19 4v16" />
|
|
148
|
+
</svg>
|
|
149
|
+
</button>
|
|
150
|
+
)}
|
|
125
151
|
</div>
|
|
126
152
|
|
|
127
153
|
{/* Tab content */}
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
CaretRight,
|
|
54
54
|
ClipboardText,
|
|
55
55
|
ArrowCounterClockwise,
|
|
56
|
+
Camera as PhCamera,
|
|
56
57
|
Gear,
|
|
57
58
|
} from "@phosphor-icons/react";
|
|
58
59
|
import type { Icon as PhosphorIcon, IconProps as PhosphorIconProps } from "@phosphor-icons/react";
|
|
@@ -127,4 +128,5 @@ export const ChevronDown = makeIcon(CaretDown);
|
|
|
127
128
|
export const ChevronRight = makeIcon(CaretRight);
|
|
128
129
|
export const ClipboardList = makeIcon(ClipboardText);
|
|
129
130
|
export const RotateCcw = makeIcon(ArrowCounterClockwise);
|
|
131
|
+
export const Camera = makeIcon(PhCamera);
|
|
130
132
|
export const Settings = makeIcon(Gear);
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { useRef, useState, useCallback, useEffect, memo } from "react";
|
|
2
2
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
3
|
-
import {
|
|
4
|
-
TIMELINE_TOGGLE_SHORTCUT_LABEL,
|
|
5
|
-
getTimelineToggleTitle,
|
|
6
|
-
} from "../../utils/timelineDiscovery";
|
|
7
|
-
import { formatFrameTime, frameToSeconds, formatTime } from "../lib/time";
|
|
3
|
+
import { formatFrameTime, frameToSeconds, stepFrameTime, formatTime } from "../lib/time";
|
|
8
4
|
import { usePlayerStore, liveTime } from "../store/playerStore";
|
|
9
5
|
|
|
10
6
|
const SPEED_OPTIONS = [0.25, 0.5, 1, 1.5, 2] as const;
|
|
@@ -30,15 +26,11 @@ export function resolveSeekPercent(clientX: number, rectLeft: number, rectWidth:
|
|
|
30
26
|
interface PlayerControlsProps {
|
|
31
27
|
onTogglePlay: () => void;
|
|
32
28
|
onSeek: (time: number) => void;
|
|
33
|
-
timelineVisible?: boolean;
|
|
34
|
-
onToggleTimeline?: () => void;
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
export const PlayerControls = memo(function PlayerControls({
|
|
38
32
|
onTogglePlay,
|
|
39
33
|
onSeek,
|
|
40
|
-
timelineVisible,
|
|
41
|
-
onToggleTimeline,
|
|
42
34
|
}: PlayerControlsProps) {
|
|
43
35
|
// Subscribe to only the fields we render — each selector prevents cascading re-renders
|
|
44
36
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
@@ -216,10 +208,10 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
216
208
|
const step = e.shiftKey ? 10 : 1;
|
|
217
209
|
if (e.key === "ArrowLeft") {
|
|
218
210
|
e.preventDefault();
|
|
219
|
-
onSeek(
|
|
211
|
+
onSeek(stepFrameTime(currentTimeRef.current, -step));
|
|
220
212
|
} else if (e.key === "ArrowRight") {
|
|
221
213
|
e.preventDefault();
|
|
222
|
-
onSeek(Math.min(duration, currentTimeRef.current
|
|
214
|
+
onSeek(Math.min(duration, stepFrameTime(currentTimeRef.current, step)));
|
|
223
215
|
}
|
|
224
216
|
},
|
|
225
217
|
[timelineReady, duration, onSeek],
|
|
@@ -437,39 +429,6 @@ export const PlayerControls = memo(function PlayerControls({
|
|
|
437
429
|
</span>
|
|
438
430
|
))}
|
|
439
431
|
</div>
|
|
440
|
-
|
|
441
|
-
{/* Timeline toggle */}
|
|
442
|
-
{onToggleTimeline !== undefined && (
|
|
443
|
-
<button
|
|
444
|
-
type="button"
|
|
445
|
-
onClick={onToggleTimeline}
|
|
446
|
-
className={`h-7 flex items-center gap-1.5 rounded-md border px-2.5 text-[11px] font-medium transition-colors ${
|
|
447
|
-
timelineVisible
|
|
448
|
-
? "text-studio-accent bg-studio-accent/10 border-studio-accent/30"
|
|
449
|
-
: "border-neutral-700 text-neutral-300 hover:border-neutral-500 hover:bg-neutral-800"
|
|
450
|
-
}`}
|
|
451
|
-
title={getTimelineToggleTitle(Boolean(timelineVisible))}
|
|
452
|
-
aria-label={timelineVisible ? "Hide timeline editor" : "Show timeline editor"}
|
|
453
|
-
>
|
|
454
|
-
<svg
|
|
455
|
-
width="13"
|
|
456
|
-
height="13"
|
|
457
|
-
viewBox="0 0 24 24"
|
|
458
|
-
fill="none"
|
|
459
|
-
stroke="currentColor"
|
|
460
|
-
strokeWidth="2"
|
|
461
|
-
strokeLinecap="round"
|
|
462
|
-
>
|
|
463
|
-
<rect x="3" y="13" width="18" height="8" rx="1" />
|
|
464
|
-
<line x1="3" y1="9" x2="21" y2="9" />
|
|
465
|
-
<line x1="3" y1="5" x2="21" y2="5" />
|
|
466
|
-
</svg>
|
|
467
|
-
<span>Timeline</span>
|
|
468
|
-
<span className="hidden md:inline rounded bg-black/20 px-1 py-0.5 text-[9px] font-mono opacity-70">
|
|
469
|
-
{TIMELINE_TOGGLE_SHORTCUT_LABEL}
|
|
470
|
-
</span>
|
|
471
|
-
</button>
|
|
472
|
-
)}
|
|
473
432
|
</div>
|
|
474
433
|
);
|
|
475
434
|
});
|
|
@@ -63,12 +63,12 @@ const TRACK_STYLES: Record<string, TimelineTrackStyle> = {
|
|
|
63
63
|
const DEFAULT_TRACK_STYLE: TimelineTrackStyle = createTrackStyle();
|
|
64
64
|
|
|
65
65
|
export const defaultTimelineTheme: TimelineTheme = {
|
|
66
|
-
shellBackground: "#
|
|
66
|
+
shellBackground: "#0A0A0B",
|
|
67
67
|
shellBorder: "rgba(255,255,255,0.05)",
|
|
68
68
|
rulerBorder: "rgba(255,255,255,0.045)",
|
|
69
|
-
rowBackground: "#
|
|
69
|
+
rowBackground: "#0A0A0B",
|
|
70
70
|
rowBorder: "rgba(255,255,255,0.05)",
|
|
71
|
-
gutterBackground: "#
|
|
71
|
+
gutterBackground: "#0A0A0B",
|
|
72
72
|
gutterBorder: "rgba(255,255,255,0.05)",
|
|
73
73
|
textPrimary: "#E8EDF5",
|
|
74
74
|
textSecondary: "#8391A8",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useCallback } from "react";
|
|
2
2
|
import { usePlayerStore, liveTime, type TimelineElement } from "../store/playerStore";
|
|
3
3
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
4
|
-
import {
|
|
4
|
+
import { stepFrameTime, STUDIO_PREVIEW_FPS } from "../lib/time";
|
|
5
5
|
import { useCaptionStore } from "../../captions/store";
|
|
6
6
|
|
|
7
7
|
interface PlaybackAdapter {
|
|
@@ -697,7 +697,7 @@ export function useTimelinePlayer() {
|
|
|
697
697
|
(deltaFrames: number) => {
|
|
698
698
|
const adapter = getAdapter();
|
|
699
699
|
const currentTime = adapter?.getTime() ?? usePlayerStore.getState().currentTime;
|
|
700
|
-
seek(currentTime
|
|
700
|
+
seek(stepFrameTime(currentTime, deltaFrames, STUDIO_PREVIEW_FPS));
|
|
701
701
|
},
|
|
702
702
|
[getAdapter, seek],
|
|
703
703
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatFrameTime, frameToSeconds, secondsToFrame, formatTime } from "./time";
|
|
2
|
+
import { formatFrameTime, frameToSeconds, secondsToFrame, stepFrameTime, formatTime } from "./time";
|
|
3
3
|
|
|
4
4
|
describe("formatTime", () => {
|
|
5
5
|
it("formats zero seconds", () => {
|
|
@@ -72,4 +72,14 @@ describe("frame helpers", () => {
|
|
|
72
72
|
it("formats current and total frame display", () => {
|
|
73
73
|
expect(formatFrameTime(1, 5)).toBe("30f / 150f");
|
|
74
74
|
});
|
|
75
|
+
|
|
76
|
+
it("steps from a truncated runtime time by integer frame index", () => {
|
|
77
|
+
expect(stepFrameTime(0.0333333, 1)).toBe(2 / 30);
|
|
78
|
+
expect(stepFrameTime(0.0666666, 1)).toBe(3 / 30);
|
|
79
|
+
expect(stepFrameTime(0.0666666, -1)).toBe(1 / 30);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("clamps frame stepping at zero", () => {
|
|
83
|
+
expect(stepFrameTime(0, -1)).toBe(0);
|
|
84
|
+
});
|
|
75
85
|
});
|
package/src/player/lib/time.ts
CHANGED
|
@@ -19,6 +19,12 @@ export function frameToSeconds(frame: number, fps = STUDIO_PREVIEW_FPS): number
|
|
|
19
19
|
return frame / fps;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function stepFrameTime(time: number, deltaFrames: number, fps = STUDIO_PREVIEW_FPS): number {
|
|
23
|
+
const currentFrame = secondsToFrame(time, fps);
|
|
24
|
+
const nextFrame = Math.max(0, currentFrame + deltaFrames);
|
|
25
|
+
return frameToSeconds(nextFrame, fps);
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
export function formatFrameTime(time: number, duration: number, fps = STUDIO_PREVIEW_FPS): string {
|
|
23
29
|
const currentFrame = secondsToFrame(time, fps);
|
|
24
30
|
const totalFrames = secondsToFrame(duration, fps);
|