Add timeline junction transitions (#15)
* Improve editor timeline and media UX * Add timeline junction transitions --------- Co-authored-by: haixin.yang <haixin.yang@weimob.com>
This commit is contained in:
committed by
GitHub
co-authored by
haixin.yang
parent
00c454dbb7
commit
51fecbbfc9
+3
-1
@@ -199,7 +199,7 @@ export function App() {
|
||||
captionTargetDuration, captionTimeline, currentCaption, currentCaptionSegment,
|
||||
currentSegmentIndex, currentStickerSegment, currentStickerSegmentIndex,
|
||||
currentVisualRange, currentVisualSegment, currentVisualSegmentIndex, estimatedDuration,
|
||||
focusedSegmentIndex, getStickerDragAsset, peaks, previewSticker, previewStickers,
|
||||
focusedSegmentIndex, getStickerDragAsset, peaks, previewSticker, previewStickers, previewTransition,
|
||||
previewVisionBaseAnalysis, previewVisionKey, previewVisionRecord, previewVisualLocalTime,
|
||||
previewVisualRange, previewVisualSegment, previewVisualSegmentIndex,
|
||||
previewVisualSourceTime, previewVisualSrc, previewVisualType, ratio, segments,
|
||||
@@ -723,6 +723,7 @@ export function App() {
|
||||
previewVisualRenderSrc={previewVisualRenderSrc}
|
||||
previewVisionMaskUrl={previewVisionMaskUrl}
|
||||
previewVisualType={previewVisualType}
|
||||
previewTransition={previewTransition}
|
||||
visualEffects={visualAnimationPreview?.segmentId && visualAnimationPreview.segmentId === previewVisualSegment?.id
|
||||
? { ...previewVisualSegment, animation: visualAnimationPreview.animation }
|
||||
: previewVisualSegment}
|
||||
@@ -908,6 +909,7 @@ export function App() {
|
||||
setSelectedStickerSegmentId={setSelectedStickerSegmentId}
|
||||
imageSrc={imageSrc}
|
||||
displayedVisualSegments={displayedVisualSegments}
|
||||
setVisualSegments={setVisualSegments}
|
||||
renderedVisualTimeline={renderedVisualTimeline}
|
||||
visualType={visualType}
|
||||
currentVisualSegment={currentVisualSegment}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FrameCorners,
|
||||
Pause,
|
||||
Play,
|
||||
Resize,
|
||||
SkipBack,
|
||||
SkipForward,
|
||||
} from "@phosphor-icons/react";
|
||||
@@ -25,6 +26,7 @@ export function PreviewStage({
|
||||
previewVisualRenderSrc,
|
||||
previewVisionMaskUrl = "",
|
||||
previewVisualType,
|
||||
previewTransition = null,
|
||||
previewRatio,
|
||||
previewFrameStyle,
|
||||
previewFrameSize,
|
||||
@@ -275,6 +277,16 @@ export function PreviewStage({
|
||||
/> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{previewTransition?.next?.src && trackVisibility.image ? (
|
||||
<div className={`preview-transition-layer type-${previewTransition.id}`} style={{ "--transition-progress": previewTransition.progress }}>
|
||||
{previewTransition.next.type === "video" ? (
|
||||
<video src={previewTransition.next.src} muted playsInline autoPlay preload="auto" style={{ objectFit: activeObjectFit, objectPosition: activeObjectPosition }} />
|
||||
) : (
|
||||
<img src={previewTransition.next.src} alt="" style={{ objectFit: activeObjectFit, objectPosition: activeObjectPosition }} />
|
||||
)}
|
||||
{previewTransition.id === "flash" ? <i /> : null}
|
||||
</div>
|
||||
) : null}
|
||||
{showVisionOverlays
|
||||
? visionOverlayBoxes.map((detection, index) => (
|
||||
<div
|
||||
@@ -334,7 +346,9 @@ export function PreviewStage({
|
||||
<img className="sticker-overlay-image" src={sticker.src} alt="" draggable={false} />
|
||||
{isEditable ? <>
|
||||
<button className="sticker-rotate-handle" type="button" aria-label={t("visualRotation", "旋转")} onPointerDown={(event) => startStickerTransform(event, "rotate", sticker)} />
|
||||
<button className="sticker-scale-handle" type="button" aria-label={t("visualScale", "缩放")} onPointerDown={(event) => startStickerTransform(event, "scale", sticker)} />
|
||||
<button className="sticker-scale-handle" type="button" aria-label={t("visualScale", "缩放")} onPointerDown={(event) => startStickerTransform(event, "scale", sticker)}>
|
||||
<Resize size={12} weight="bold" aria-hidden="true" />
|
||||
</button>
|
||||
</> : null}
|
||||
</div>
|
||||
) : sticker.text ? (
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Waveform,
|
||||
} from "@phosphor-icons/react";
|
||||
|
||||
import { IMAGE_SEGMENT_SECONDS, MAX_IMAGE_THUMBNAILS } from "../config/editor.js";
|
||||
import { IMAGE_SEGMENT_SECONDS, MAX_IMAGE_THUMBNAILS, TRANSITIONS } from "../config/editor.js";
|
||||
import { formatClock, formatTime, getSegmentStartTime, packTimedSegmentsIntoLanes } from "../lib/timeline.js";
|
||||
import { sliceSourceAudioPeaks } from "../lib/sourceAudioSync.js";
|
||||
import {
|
||||
@@ -163,6 +163,7 @@ export function Timeline({
|
||||
setSelectedStickerSegmentId,
|
||||
imageSrc,
|
||||
displayedVisualSegments,
|
||||
setVisualSegments,
|
||||
renderedVisualTimeline,
|
||||
visualType,
|
||||
currentVisualSegment,
|
||||
@@ -205,6 +206,13 @@ export function Timeline({
|
||||
musicDuration,
|
||||
startMusicMove,
|
||||
}) {
|
||||
const [transitionEditor, setTransitionEditor] = useState(null);
|
||||
|
||||
const updateJunctionTransition = (index, patch) => {
|
||||
setVisualSegments((items) => items.map((item, itemIndex) => itemIndex === index
|
||||
? { ...item, transition: { id: item.transition?.id || "none", duration: item.transition?.duration || 0.5, ...patch } }
|
||||
: item));
|
||||
};
|
||||
const draggingVisualSegment =
|
||||
activeTimelineClipDrag?.track === "image"
|
||||
? displayedVisualSegments.find((segment) => segment.id === activeTimelineClipDrag.segmentId)
|
||||
@@ -937,6 +945,26 @@ export function Timeline({
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{displayedVisualSegments.slice(0, -1).map((segment, index) => {
|
||||
const range = renderedVisualTimeline[index];
|
||||
const transition = segment.transition || { id: "none", duration: 0.5 };
|
||||
return (
|
||||
<button
|
||||
className={`visual-junction ${transition.id !== "none" ? "has-transition" : ""}`}
|
||||
key={`junction-${segment.id}`}
|
||||
type="button"
|
||||
aria-label={`转场:${TRANSITIONS.find((item) => item.id === transition.id)?.name || "无转场"}`}
|
||||
title="设置转场"
|
||||
style={{ left: `${((range?.end || 0) / Math.max(0.01, timelineDuration)) * 100}%` }}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
setTransitionEditor({ index, x: rect.left + rect.width / 2, y: rect.top });
|
||||
}}
|
||||
><span>◇</span></button>
|
||||
);
|
||||
})}
|
||||
{renderAssetDropSlot("image")}
|
||||
</div>
|
||||
{showStickerTrack ? stickerLanes.map((lane, laneIndex) => renderStickerTrack(lane, laneIndex)) : null}
|
||||
@@ -1189,6 +1217,27 @@ export function Timeline({
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{transitionEditor ? (() => {
|
||||
const segment = displayedVisualSegments[transitionEditor.index];
|
||||
const transition = segment?.transition || { id: "none", duration: 0.5 };
|
||||
const maxDuration = Math.max(0.1, Math.min(2, (segment?.duration || 0.5) / 2, (displayedVisualSegments[transitionEditor.index + 1]?.duration || 0.5) / 2));
|
||||
return (
|
||||
<div className="transition-popover" role="dialog" aria-label="转场设置" style={{ left: transitionEditor.x, top: transitionEditor.y }} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<div className="transition-popover-head"><strong>转场</strong><button type="button" onClick={() => setTransitionEditor(null)} aria-label="关闭">×</button></div>
|
||||
<div className="transition-presets">
|
||||
{TRANSITIONS.map((option) => (
|
||||
<button type="button" className={transition.id === option.id ? "is-selected" : ""} key={option.id} onClick={() => updateJunctionTransition(transitionEditor.index, { id: option.id })}>
|
||||
<i className={`transition-preview preview-${option.id}`} /><span>{option.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<label className="transition-duration-control">
|
||||
<span><b>时长</b><em>{Math.min(maxDuration, transition.duration || 0.5).toFixed(1)}s</em></span>
|
||||
<input type="range" min="0.1" max={maxDuration} step="0.1" value={Math.min(maxDuration, transition.duration || 0.5)} disabled={transition.id === "none"} onChange={(event) => updateJunctionTransition(transitionEditor.index, { duration: Number(event.target.value) })} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})() : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
STICKERS,
|
||||
STICKER_CATEGORIES,
|
||||
STICKER_PAGE_SIZE,
|
||||
TRANSITIONS,
|
||||
VOICES,
|
||||
} from "../config/editor.js";
|
||||
import { APP_LANGUAGES } from "../i18n.js";
|
||||
@@ -509,23 +508,6 @@ export function ToolPanel(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTool === "transition") {
|
||||
return (
|
||||
<VisualChoicePanel
|
||||
title={t("transition")}
|
||||
kind="transition"
|
||||
options={TRANSITIONS}
|
||||
selectedId={selectedTransitionId}
|
||||
trOption={trOption}
|
||||
sourceAudioLinked={sourceAudioLinked}
|
||||
onSelect={(id) => {
|
||||
setSelectedTransitionId(id);
|
||||
notify(t("transitionApplied"));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTool === "effects") {
|
||||
return (
|
||||
<VisualEffectsPanel
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CirclesThree,
|
||||
ClosedCaptioning,
|
||||
ImageSquare,
|
||||
MusicNote,
|
||||
@@ -162,7 +161,6 @@ export const TOOL_RAIL = [
|
||||
{ id: "caption", label: "字幕", icon: ClosedCaptioning },
|
||||
{ id: "smart", label: "智能", icon: Scan },
|
||||
{ id: "audio", label: "音频", icon: MusicNote },
|
||||
{ id: "transition", label: "转场", icon: CirclesThree },
|
||||
{ id: "stickers", label: "贴纸", icon: Sticker },
|
||||
];
|
||||
|
||||
|
||||
@@ -177,6 +177,15 @@ export function useTimelineModel(d) {
|
||||
const previewVisualSourceTime = previewVisualType === "video"
|
||||
? getVisualSourceTime(previewVisualSegment, previewVisualLocalTime)
|
||||
: previewVisualLocalTime;
|
||||
const previewTransition = (() => {
|
||||
const junction = previewVisualSegment?.transition;
|
||||
const next = d.visualSegments[previewVisualSegmentIndex + 1];
|
||||
if (!next || !junction?.id || junction.id === "none" || !previewVisualRange) return null;
|
||||
const duration = Math.max(0.1, Math.min(Number(junction.duration) || 0.5, previewVisualSegment.duration || 0.5, next.duration || 0.5));
|
||||
const start = previewVisualRange.end - duration;
|
||||
if (d.currentTime < start || d.currentTime >= previewVisualRange.end) return null;
|
||||
return { id: junction.id, duration, progress: (d.currentTime - start) / duration, next };
|
||||
})();
|
||||
const previewVisionKey = getVisionKey(previewVisualSegment ?? (previewVisualSrc ? {
|
||||
id: "visual-fallback",
|
||||
src: previewVisualSrc,
|
||||
@@ -207,7 +216,7 @@ export function useTimelineModel(d) {
|
||||
currentStickerSegmentIndex, currentVisualRange, currentVisualSegment,
|
||||
currentVisualSegmentIndex, estimatedDuration, focusedSegmentIndex, getStickerDragAsset,
|
||||
peaks, previewSticker, previewStickers, previewVisionBaseAnalysis, previewVisionKey,
|
||||
previewVisionRecord, previewVisualLocalTime, previewVisualRange, previewVisualSegment,
|
||||
previewTransition, previewVisionRecord, previewVisualLocalTime, previewVisualRange, previewVisualSegment,
|
||||
previewVisualSegmentIndex, previewVisualSourceTime, previewVisualSrc, previewVisualType,
|
||||
ratio, segments, selectedAudioSegment, selectedCaptionSegment, selectedFilter,
|
||||
selectedSegmentIndex, selectedSticker, selectedStickerSegmentIndex,
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useVideoExport(d) {
|
||||
: { width: (360 * d.ratio.width) / d.ratio.height, height: 360 },
|
||||
sticker: d.stickerSegments.length ? null : d.selectedSticker,
|
||||
stickerSegments: d.trackVisibility.sticker ? d.stickerSegments : [],
|
||||
transitionId: d.selectedTransitionId, exportSettings: d.exportSettings, onProgress: progress,
|
||||
transitionId: "none", exportSettings: d.exportSettings, onProgress: progress,
|
||||
});
|
||||
const name = `ai-voiceover-${d.ratio.id.replace(":", "x")}`;
|
||||
if (d.exportSettings.codec !== "h264") {
|
||||
|
||||
+44
-16
@@ -539,7 +539,6 @@ function drawFittedVisual(context, visual, canvas, fitMode, filter, vision = nul
|
||||
function drawPreviewFrame(context, visual, canvas, options) {
|
||||
const {
|
||||
subtitle,
|
||||
progress = 0,
|
||||
fitMode = "contain",
|
||||
filter = "none",
|
||||
captionsEnabled = true,
|
||||
@@ -553,6 +552,8 @@ function drawPreviewFrame(context, visual, canvas, options) {
|
||||
stickers = [],
|
||||
stickerImages = [],
|
||||
transitionId = "none",
|
||||
transitionNext = null,
|
||||
transitionProgress = 0,
|
||||
vision = null,
|
||||
visualEffects = null,
|
||||
visualTime = 0,
|
||||
@@ -616,6 +617,33 @@ function drawPreviewFrame(context, visual, canvas, options) {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
if (transitionNext?.visual && transitionId !== "none" && transitionProgress > 0) {
|
||||
const amount = Math.max(0, Math.min(1, transitionProgress));
|
||||
context.save();
|
||||
if (transitionId === "wipe-left") context.rect(width * (1 - amount), 0, width * amount, height);
|
||||
else if (transitionId === "wipe-up") context.rect(0, height * (1 - amount), width, height * amount);
|
||||
else if (transitionId === "split") {
|
||||
context.rect(width * (0.5 - amount / 2), 0, width * amount, height);
|
||||
}
|
||||
if (["wipe-left", "wipe-up", "split"].includes(transitionId)) context.clip();
|
||||
context.globalAlpha = transitionId === "flash" ? Math.max(0, (amount - 0.35) / 0.65) : amount;
|
||||
if (transitionId === "zoom") {
|
||||
const scale = 1.12 - amount * 0.12;
|
||||
context.translate(width / 2, height / 2); context.scale(scale, scale); context.translate(-width / 2, -height / 2);
|
||||
}
|
||||
const nextFilter = transitionId === "blur" ? `blur(${Math.max(0, (1 - amount) * 14)}px)` : transitionNext.filter || filter;
|
||||
drawFittedVisual(context, transitionNext.visual, canvas, fitMode, nextFilter, transitionNext.vision || null);
|
||||
context.restore();
|
||||
if (transitionId === "flash") {
|
||||
context.fillStyle = `rgba(255,255,255,${Math.max(0, 1 - Math.abs(amount - 0.5) * 2) * 0.7})`;
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
if (transitionId === "glitch") {
|
||||
context.fillStyle = `rgba(53,234,217,${Math.sin(amount * Math.PI * 8) * 0.12})`;
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
if (captionsEnabled && subtitle) {
|
||||
const captionLayout = getCaptionTextLayout({
|
||||
context,
|
||||
@@ -679,20 +707,6 @@ function drawPreviewFrame(context, visual, canvas, options) {
|
||||
}
|
||||
});
|
||||
|
||||
if (transitionId === "fade") {
|
||||
const fadeAlpha =
|
||||
progress < 0.12 ? 1 - progress / 0.12 : progress > 0.88 ? (progress - 0.88) / 0.12 : 0;
|
||||
if (fadeAlpha > 0) {
|
||||
context.fillStyle = `rgba(0, 0, 0, ${Math.min(0.72, fadeAlpha * 0.72)})`;
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
if (transitionId === "flash" && progress < 0.08) {
|
||||
context.fillStyle = `rgba(255, 255, 255, ${0.45 * (1 - progress / 0.08)})`;
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function getSupportedRecordingFormat() {
|
||||
@@ -1051,6 +1065,18 @@ export async function exportBrowserVideo({
|
||||
const visualSourceTime = syncVideoItem(visualItem, localTime);
|
||||
const exportStickers = getStickersAtTime(elapsed);
|
||||
const exportVisual = visualItem.cutoutVisual || visualItem.visual;
|
||||
const visualIndex = visualItems.indexOf(visualItem);
|
||||
const junction = visualItem.segment.transition;
|
||||
const transitionDuration = junction?.id && junction.id !== "none"
|
||||
? Math.max(0.1, Math.min(Number(junction.duration) || 0.5, (visualRange?.end || 0) - (visualRange?.start || 0)))
|
||||
: 0;
|
||||
const transitionStart = (visualRange?.end || 0) - transitionDuration;
|
||||
const nextVisualItem = transitionDuration > 0 && elapsed >= transitionStart ? visualItems[visualIndex + 1] : null;
|
||||
const transitionProgress = nextVisualItem ? (elapsed - transitionStart) / transitionDuration : 0;
|
||||
if (nextVisualItem?.segment.type === "video") {
|
||||
const nextTime = Math.max(0, Number(nextVisualItem.segment.sourceStart) || 0) + transitionProgress * transitionDuration;
|
||||
if (!nextVisualItem.visual.seeking && Math.abs(nextVisualItem.visual.currentTime - nextTime) > 0.05) nextVisualItem.visual.currentTime = nextTime;
|
||||
}
|
||||
const resolvedVision = resolveVisionAnalysisAtTime(
|
||||
visualItem.segment.vision ?? null,
|
||||
visualSourceTime,
|
||||
@@ -1077,7 +1103,9 @@ export async function exportBrowserVideo({
|
||||
captionReferenceSize,
|
||||
stickers: exportStickers,
|
||||
stickerImages: exportStickers.map((item) => item?.src ? stickerImageMap.get(item.src) : null),
|
||||
transitionId,
|
||||
transitionId: nextVisualItem ? junction.id : "none",
|
||||
transitionNext: nextVisualItem ? { visual: nextVisualItem.cutoutVisual || nextVisualItem.visual } : null,
|
||||
transitionProgress,
|
||||
vision: frameVision,
|
||||
visualEffects: visualItem.segment,
|
||||
visualTime: localTime,
|
||||
|
||||
+84
-5
@@ -2508,8 +2508,8 @@ button:disabled {
|
||||
}
|
||||
.visual-media-layer > .remaster-preview-frame { z-index: 2; }
|
||||
|
||||
.preview-frame img,
|
||||
.preview-frame video {
|
||||
.visual-media-layer > img,
|
||||
.visual-media-layer > video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -2518,8 +2518,8 @@ button:disabled {
|
||||
background: #080a0e;
|
||||
}
|
||||
|
||||
.preview-canvas.fit-cover .preview-frame img,
|
||||
.preview-canvas.fit-cover .preview-frame video {
|
||||
.preview-canvas.fit-cover .visual-media-layer > img,
|
||||
.preview-canvas.fit-cover .visual-media-layer > video {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
@@ -2701,7 +2701,8 @@ button:disabled {
|
||||
.sticker-scale-handle { position: absolute; z-index: 2; width: 14px; height: 14px; border: 2px solid #081315; border-radius: 50%; padding: 0; background: #35ead9; box-shadow: 0 0 0 1px rgba(255,255,255,.72); }
|
||||
.sticker-rotate-handle { top: -23px; left: 50%; cursor: grab; transform: translateX(-50%); }
|
||||
.sticker-rotate-handle::after { position: absolute; top: 11px; left: 5px; width: 2px; height: 10px; background: #35ead9; content: ""; }
|
||||
.sticker-scale-handle { right: -8px; bottom: -8px; cursor: nwse-resize; }
|
||||
.sticker-scale-handle { right: -11px; bottom: -11px; display: grid; place-items: center; width: 22px; height: 22px; border-radius: 6px; color: #071311; cursor: nwse-resize; }
|
||||
.sticker-scale-handle svg { pointer-events: none; }
|
||||
|
||||
.sticker-properties-panel { display: grid; gap: 14px; }
|
||||
.sticker-properties-preview { display: grid; place-items: center; min-height: 92px; border: 1px solid rgba(255,255,255,.07); border-radius: 9px; background: rgba(255,255,255,.025); }
|
||||
@@ -3734,11 +3735,89 @@ button:disabled {
|
||||
}
|
||||
|
||||
.image-track {
|
||||
position: relative;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.visual-junction {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
z-index: 8;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.42);
|
||||
border-radius: 6px;
|
||||
color: #e8f1f5;
|
||||
background: #202933;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.42);
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.visual-junction:hover,
|
||||
.visual-junction.has-transition {
|
||||
border-color: #35ead9;
|
||||
color: #061513;
|
||||
background: #35ead9;
|
||||
}
|
||||
|
||||
.visual-junction span { font-size: 15px; line-height: 1; transform: rotate(45deg); }
|
||||
|
||||
.transition-popover {
|
||||
position: fixed;
|
||||
z-index: 80;
|
||||
width: 320px;
|
||||
padding: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
border-radius: 12px;
|
||||
color: #edf7f8;
|
||||
background: rgba(19, 25, 32, 0.98);
|
||||
box-shadow: 0 18px 55px rgba(0, 0, 0, 0.55);
|
||||
transform: translate(-50%, calc(-100% - 12px));
|
||||
}
|
||||
|
||||
.transition-popover-head,
|
||||
.transition-duration-control > span { display: flex; align-items: center; justify-content: space-between; }
|
||||
.transition-popover-head { margin-bottom: 12px; }
|
||||
.transition-popover-head strong { font-size: 14px; }
|
||||
.transition-popover-head button { width: 26px; height: 26px; border: 0; border-radius: 6px; color: #9baab5; background: transparent; cursor: pointer; }
|
||||
.transition-popover-head button:hover { color: #fff; background: rgba(255,255,255,.08); }
|
||||
|
||||
.transition-presets { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
|
||||
.transition-presets > button { min-width: 0; padding: 8px 5px; border: 1px solid rgba(255,255,255,.08); border-radius: 8px; color: #aebbc4; background: rgba(255,255,255,.035); cursor: pointer; }
|
||||
.transition-presets > button:hover,
|
||||
.transition-presets > button.is-selected { border-color: rgba(53,234,217,.72); color: #eafffd; background: rgba(53,234,217,.09); }
|
||||
.transition-presets span { display: block; margin-top: 5px; overflow: hidden; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.transition-preview { display: block; width: 100%; height: 24px; border-radius: 4px; background: linear-gradient(90deg,#527eb0 0 48%,#c17c65 52% 100%); }
|
||||
.transition-preview.preview-fade { background: linear-gradient(90deg,#527eb0,#20252b,#c17c65); }
|
||||
.transition-preview.preview-flash { background: linear-gradient(90deg,#527eb0,#fff,#c17c65); }
|
||||
.transition-preview.preview-wipe-left,
|
||||
.transition-preview.preview-wipe-up { background: linear-gradient(120deg,#527eb0 0 45%,#c17c65 47% 100%); }
|
||||
.transition-preview.preview-blur { filter: blur(1px); }
|
||||
|
||||
.transition-duration-control { display: block; margin-top: 13px; }
|
||||
.transition-duration-control span { color: #b8c4cc; font-size: 12px; }
|
||||
.transition-duration-control em { color: #35ead9; font-style: normal; font-variant-numeric: tabular-nums; }
|
||||
.transition-duration-control input { width: 100%; margin-top: 8px; accent-color: #35ead9; }
|
||||
|
||||
.preview-transition-layer { position: absolute; inset: 0; z-index: 2; overflow: hidden; opacity: var(--transition-progress); pointer-events: none; }
|
||||
.preview-transition-layer img,
|
||||
.preview-transition-layer video { width: 100%; height: 100%; display: block; }
|
||||
.preview-transition-layer.type-wipe-left { clip-path: inset(0 0 0 calc((1 - var(--transition-progress)) * 100%)); opacity: 1; }
|
||||
.preview-transition-layer.type-wipe-up { clip-path: inset(calc((1 - var(--transition-progress)) * 100%) 0 0); opacity: 1; }
|
||||
.preview-transition-layer.type-split { clip-path: inset(0 calc((1 - var(--transition-progress)) * 50%)); opacity: 1; }
|
||||
.preview-transition-layer.type-zoom img,
|
||||
.preview-transition-layer.type-zoom video { transform: scale(calc(1.12 - var(--transition-progress) * .12)); }
|
||||
.preview-transition-layer.type-blur img,
|
||||
.preview-transition-layer.type-blur video { filter: blur(calc((1 - var(--transition-progress)) * 12px)); }
|
||||
.preview-transition-layer.type-flash i { position: absolute; inset: 0; opacity: .7; background: #fff; }
|
||||
|
||||
.image-track.is-reordering,
|
||||
.caption-track.is-reordering {
|
||||
cursor: grabbing;
|
||||
|
||||
Reference in New Issue
Block a user