@meframe/core 0.5.0 → 0.5.2
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/model/CompositionModel.d.ts +6 -0
- package/dist/model/CompositionModel.d.ts.map +1 -1
- package/dist/model/CompositionModel.js +27 -2
- package/dist/model/CompositionModel.js.map +1 -1
- package/dist/model/types.d.ts +8 -2
- package/dist/model/types.d.ts.map +1 -1
- package/dist/model/types.js.map +1 -1
- package/dist/orchestrator/CompositionPlanner.d.ts +1 -0
- package/dist/orchestrator/CompositionPlanner.d.ts.map +1 -1
- package/dist/orchestrator/CompositionPlanner.js +22 -1
- package/dist/orchestrator/CompositionPlanner.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +8 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
- package/dist/stages/compose/LayerRenderer.js +20 -9
- package/dist/stages/compose/LayerRenderer.js.map +1 -1
- package/dist/stages/compose/font-system/FontManager.d.ts.map +1 -1
- package/dist/stages/compose/font-system/FontManager.js +6 -2
- package/dist/stages/compose/font-system/FontManager.js.map +1 -1
- package/dist/stages/compose/font-system/types.d.ts +1 -1
- package/dist/stages/compose/font-system/types.d.ts.map +1 -1
- package/dist/stages/compose/instructions.d.ts +1 -1
- package/dist/stages/compose/instructions.d.ts.map +1 -1
- package/dist/stages/compose/text-layer-animation-time.d.ts +8 -0
- package/dist/stages/compose/text-layer-animation-time.d.ts.map +1 -0
- package/dist/stages/compose/text-layer-animation-time.js +22 -0
- package/dist/stages/compose/text-layer-animation-time.js.map +1 -0
- package/dist/stages/compose/text-renderers/basic-text-renderer.js +1 -21
- package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -1
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts +9 -0
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.d.ts.map +1 -0
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js +188 -0
- package/dist/stages/compose/text-renderers/caption-stagger-entrance-renderer.js.map +1 -0
- package/dist/stages/compose/text-renderers/word-fancy-renderer.js +1 -1
- package/dist/stages/compose/text-utils/caption-layout.d.ts +4 -0
- package/dist/stages/compose/text-utils/caption-layout.d.ts.map +1 -1
- package/dist/stages/compose/text-utils/caption-layout.js +14 -1
- package/dist/stages/compose/text-utils/caption-layout.js.map +1 -1
- package/dist/stages/compose/types.d.ts +3 -1
- package/dist/stages/compose/types.d.ts.map +1 -1
- package/dist/workers/stages/export/{export.worker.Cw9iPvkh.js → export.worker.Dztm6GuN.js} +218 -28
- package/dist/workers/stages/export/export.worker.Dztm6GuN.js.map +1 -0
- package/dist/workers/worker-manifest.json +1 -1
- package/package.json +1 -1
- package/dist/workers/stages/export/export.worker.Cw9iPvkh.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Orchestrator.js","sources":["../../src/orchestrator/Orchestrator.ts"],"sourcesContent":["import { EventBus } from '../event/EventBus';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { applyPatch as applyModelPatch } from '../model/patch';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame, Clip } from '../model';\nimport { hasResourceId, videoClipPlaybackRate } from '../model/types';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { AudioWindowPreparer } from './AudioWindowPreparer';\nimport { AudioPreviewSession } from './AudioPreviewSession';\nimport { AudioExportSession } from './AudioExportSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '../types';\nimport { VideoWindowDecodeSession } from './VideoWindowDecodeSession';\nimport { ExportScheduler } from './ExportScheduler';\nimport { filterRenderConfig } from '../utils/object-utils';\nimport { isTerminalMediaResourceError } from '../utils/errors';\nimport { LoadProgress } from '@/stages/load/types';\n\nexport class Orchestrator implements IOrchestrator {\n workers: WorkerPool;\n eventBus: EventBus<EventPayloadMap>;\n compositionModel: CompositionModel | null = null;\n resourceLoader: ResourceLoader;\n cacheManager: CacheManager;\n planner: CompositionPlanner;\n audio: {\n prepare: AudioWindowPreparer;\n preview: AudioPreviewSession;\n export: AudioExportSession;\n };\n muxManager: MuxManager;\n exportScheduler: ExportScheduler;\n\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private ensureCacheDebounceTimer: number | null = null;\n private activeDecodeSession: VideoWindowDecodeSession | null = null;\n private modelToken = 0;\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n constructor(config: OrchestratorConfig) {\n // Use provided eventBus or create a new one\n this.eventBus = config.eventBus || new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n\n // Initialize config first\n this.config = ConfigLoader.getInstance().getConfig();\n\n const workerConfigs = this.buildWorkerConfigs();\n\n // Initialize WorkerPool with worker path from config\n this.workers = new WorkerPool({\n eventBus: this.eventBus,\n workerConfigs,\n workerPath: config.workerPath,\n workerExtension: config.workerExtension,\n });\n\n const maxMemoryMB = config.cacheConfig?.l1Size || this.config.cache?.l1?.maxMemoryMB || 1024;\n const maxGOPs = this.config.decode?.video?.maxGOPs || 4;\n\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB,\n maxGOPs,\n },\n resource: {\n projectId: config.projectId || this.config.global.projectId || 'default',\n },\n },\n this.eventBus\n );\n\n this.resourceLoader = new ResourceLoader({\n cacheManager: this.cacheManager,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: this.config.load.maxConcurrent,\n preloadConcurrency: 2, // Fixed preload concurrency for idle background loading\n },\n onProgress: (progress) => this.handleResourceProgress(progress),\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n const audioPreparer = new AudioWindowPreparer({\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\n });\n\n const audioPreview = new AudioPreviewSession({\n cacheManager: this.cacheManager,\n preparer: audioPreparer,\n });\n\n const audioExport = new AudioExportSession({\n cacheManager: this.cacheManager,\n preparer: audioPreparer,\n });\n\n this.audio = { prepare: audioPreparer, preview: audioPreview, export: audioExport };\n\n this.muxManager = new MuxManager();\n\n this.exportScheduler = new ExportScheduler({\n workerPool: this.workers,\n planner: this.planner,\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n muxManager: this.muxManager,\n audioSession: this.audio.export,\n workerConfigsProvider: () => this.buildWorkerConfigs(),\n eventBus: this.eventBus,\n });\n\n this.setupResourceFirstFrameHandler();\n }\n\n private setupResourceFirstFrameHandler(): void {\n this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {\n const { resourceId, clipId, index, chunks } = payload;\n\n if (!this.compositionModel) return;\n\n // Find the specific clip\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !clip.trackId) return;\n\n // Only decode first frame for clips that start at composition time 0\n // (these clips need the resource's first frame as cover)\n if (clip.startUs === 0) {\n const fps = this.compositionModel.fps ?? 30;\n await VideoWindowDecodeSession.decodeAndCacheFirstFrame(\n resourceId,\n chunks,\n index,\n clip,\n this.cacheManager,\n fps\n );\n }\n });\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n await this.cacheManager.init();\n\n this.isInitialized = true;\n }\n\n // Event methods - forward to eventBus\n on<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.on(event, handler);\n }\n\n off<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.off(event, handler);\n }\n\n once<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.once(event, handler);\n }\n\n cancelActiveDecoding(): void {\n if (this.activeDecodeSession) {\n void this.activeDecodeSession.dispose();\n this.activeDecodeSession = null;\n }\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.cacheManager.clear();\n this.cancelActiveDecoding();\n this.audio.preview.stopPlayback();\n this.audio.preview.invalidatePreviewMixCache();\n this.modelToken++;\n this.compositionModel = model;\n this.planner.setModel(model);\n\n // IMPORTANT: avoid a window where audio.prepare still references the old model while\n // resourceLoader already switched (resourceLoader.setModel sets this.model before awaiting).\n // This matters when the host app triggers play/seek without awaiting setCompositionModel().\n const resourceLoaderSetModelPromise = this.resourceLoader.setModel(model);\n this.audio.prepare.setModel(model);\n // ensure the cover resource is preloaded before preview starts\n await resourceLoaderSetModelPromise;\n\n this.eventBus.emit(MeframeEvent.ModelSet, model);\n\n this.eventBus.emit(MeframeEvent.CompositionUpdated, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce((acc: number, track: any) => acc + track.clips.length, 0),\n durationUs: model.durationUs,\n });\n }\n\n async applyPatch(patch: CompositionPatch): Promise<void> {\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n // Apply patch and get affected clip IDs (simplified for 2-Clip strategy)\n // Note: addTrack/removeTrack already call buildIndexes() in patch.ts\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n this.planner.applyPatch(patch, affectedClipIds);\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Process clip updates\n for (const clipId of affectedClipIds) {\n this.cacheManager.invalidateClip(clipId);\n }\n\n // Reactivate updated audio clips\n const reactivatedAudioClips: string[] = [];\n const reactivatedVideoClips: string[] = [];\n for (const clipId of affectedClipIds) {\n const clip = this.compositionModel.findClip(clipId);\n if (clip?.trackKind === 'audio') {\n await this.audio.prepare.deactivateClip(clipId);\n reactivatedAudioClips.push(clipId);\n } else if (clip?.trackKind === 'video') {\n reactivatedVideoClips.push(clipId);\n }\n }\n\n // Activate all audio clips (including reactivated ones)\n await this.audio.prepare.activateAllAudioClips();\n\n // Note: No need to restart per-clip playback in new architecture\n // scheduleAudio() uses OfflineAudioMixer which automatically includes all active clips\n }\n\n private handleResourceProgress(progress: LoadProgress): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.eventBus.emit(MeframeEvent.LoadProgress, {\n resourceId: progress.resourceId,\n cached: progress.cached,\n loadedBytes: progress.bytesLoaded,\n total: progress.totalBytes,\n percentage: progress.percentage,\n speed: progress.speed > 0 ? progress.speed : 128000,\n estimatedTimeRemaining: progress.estimatedTimeRemaining,\n });\n }\n\n private handleResourceStateChange(resourceId: string, state: Resource['state']): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.compositionModel.updateResourceState(resourceId, state ?? 'pending');\n\n if (state !== 'ready') {\n return;\n }\n\n // For preview, simple cache invalidation or triggering re-render might be enough\n // if we were caching instructions. But PlaybackController pulls instructions every frame.\n // So just updating Model state is enough.\n }\n\n async getFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n const preheat = options?.preheat ?? false;\n const scrub = options?.scrub ?? false;\n const mode = options?.mode ?? 'blocking';\n\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n\n // Map timeline position within clip to resource timestamp (decoder / cache coordinates).\n const clipTimelineUs = options?.relativeTimeUs ?? timeUs - clip.startUs;\n let resourceTimeUs = trimStartUs + clipTimelineUs * playbackRate;\n\n const resourceSpanUs = clip.durationUs * playbackRate;\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + Math.max(0, resourceSpanUs) - 1);\n\n // 1. Check L1 window cache\n // Note: preheat mode skips cache check to force decoding the entire window\n // Why: Video cache is frame-level (point query), not window-level (range query)\n // A single cached frame doesn't guarantee the entire window is cached\n // Without preheat flag, preheating would return early on first frame hit\n if (!preheat) {\n const cachedFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (cachedFrame) {\n // Scrubbing prefers a true frame near the target. A cached keyframe preview can be far away\n // (VideoL1Cache returns nearest by timestamp), which makes dragging feel \"stuck on I-frames\".\n if (scrub) {\n const fps = this.compositionModel?.fps ?? 30;\n const maxDistanceUs = Math.max(1, Math.round(1_000_000 / fps));\n const cachedTs = cachedFrame.timestampUs ?? 0;\n if (Math.abs(cachedTs - resourceTimeUs) <= maxDistanceUs) {\n return cachedFrame;\n }\n } else {\n return cachedFrame;\n }\n }\n }\n\n if (signal?.aborted) {\n return null;\n }\n\n // 2. Try decode from OPFS resource (on-demand path)\n const resourceFrame = await this.decodeFromResource(clip, resourceTimeUs, timeUs, {\n ...options,\n mode,\n });\n return resourceFrame;\n }\n\n /**\n * Compose frame from OPFS resource (on-demand decoding)\n * This is the new path for long clips with window caching\n *\n * @param clip - The clip to decode\n * @param resourceTimeUs - Time in resource-relative coordinates (same as frame.timestamp)\n * @param globalTimeUs - Time in composition timeline\n */\n private async decodeFromResource(\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<RcFrame | null> {\n if (!hasResourceId(clip)) return null;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n const startModelToken = this.modelToken;\n const startModel = this.compositionModel;\n\n // Check resource state\n const resource = this.compositionModel?.getResource(resourceId);\n const isReady = resource?.state === 'ready';\n const isTerminalError = resource?.state === 'error' && resource.error?.terminal === true;\n\n const fetchOptions = {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n };\n\n const mode = options?.mode ?? 'blocking';\n\n // If the resource is already in an error terminal state, do not retry loading.\n // Returning null here prevents playback start from failing and avoids repeated logs.\n if (isTerminalError) {\n return null;\n }\n\n // In probe mode, if not ready, trigger fetch in background and return null.\n // Note: If resource is already ready, probe mode behaves the same as blocking mode\n // (it may still decode from OPFS). The only guaranteed non-blocking part is resource loading.\n if (mode === 'probe' && !isReady) {\n void this.resourceLoader.load(resourceId, fetchOptions).catch(() => {});\n return null;\n }\n\n // Normal mode: wait for download\n try {\n await this.resourceLoader.load(resourceId, fetchOptions);\n } catch (error) {\n // Permanent media/container mismatch should not crash playback loops.\n // Treat as \"no frame available\" and let upper layers decide presentation.\n if (isTerminalMediaResourceError(error)) {\n console.warn('[Orchestrator] Resource not decodable, returning null frame:', {\n clipId: clip.id,\n resourceId,\n error: { name: error.name, message: error.message },\n });\n return null;\n }\n throw error;\n }\n\n if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {\n console.warn('[Orchestrator] Model switched during decodeFromResource:', {\n startModelToken,\n currentModelToken: this.modelToken,\n clipId: clip.id,\n resourceId,\n globalTimeUs,\n resourceTimeUs,\n mode,\n });\n return null;\n }\n\n this.cancelActiveDecoding();\n\n // Create temporary on-demand video session\n const session = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId,\n resourceUri: resource?.uri ?? '',\n targetTimeUs: resourceTimeUs,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n fps: this.compositionModel?.fps ?? 30,\n });\n\n this.activeDecodeSession = session;\n\n try {\n if (options?.scrub) {\n await session.decodeScrub(resourceTimeUs);\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n }\n\n // Decode window: from target position to target + 3s\n // Window bounds are in resource time: [trimStartUs, trimStartUs + durationUs)\n const DECODE_WINDOW_SIZE = 3_000_000;\n\n const playbackRate = videoClipPlaybackRate(clip);\n const resourceSpanUs = clip.durationUs * playbackRate;\n const windowStart = resourceTimeUs;\n const windowEnd = Math.min(trimStartUs + resourceSpanUs, resourceTimeUs + DECODE_WINDOW_SIZE);\n\n await session.decodeWindow(windowStart, windowEnd);\n // Return target frame from L1 cache\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n } catch (error) {\n if (session.isDisposed) {\n return null;\n }\n console.error('[Orchestrator] Error composing from resource:', error);\n return null;\n } finally {\n if (this.activeDecodeSession === session) {\n this.activeDecodeSession = null;\n }\n await session.dispose();\n }\n }\n\n /**\n * Wait for clip cache to be ready for playback\n * Returns true if minimum cache is ready, false if timeout\n */\n async waitForClipReady(\n timeUs: TimeUs,\n options?: { minFrameCount?: number; timeoutMs?: number }\n ): Promise<boolean> {\n if (!this.compositionModel) {\n return false;\n }\n\n const clips = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId);\n if (clips.length === 0) {\n return true;\n }\n\n const currentClip = clips[0];\n if (!currentClip) {\n return true;\n }\n\n return this.cacheManager.waitForClipReady(currentClip.id, {\n minFrameCount: options?.minFrameCount ?? 5,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n async dispose(): Promise<void> {\n if (this.ensureCacheDebounceTimer !== null) {\n clearTimeout(this.ensureCacheDebounceTimer);\n this.ensureCacheDebounceTimer = null;\n }\n\n this.resourceLoader.dispose();\n await this.cacheManager.clear();\n\n this.workers.terminateAll();\n this.compositionModel = null;\n this.eventBus.dispose();\n }\n\n private buildWorkerConfigs(): Record<WorkerType, any> {\n const config = this.config;\n const defaultCanvasWidth = config.global.defaultCanvasWidth;\n const defaultCanvasHeight = config.global.defaultCanvasHeight;\n const defaultFps = config.global.defaultFps;\n\n const targetFps = this.compositionModel?.fps ?? defaultFps;\n\n return {\n // videoDemux: { // DEPRECATED: Removed - replaced by IndexedVideoSource\n // highWaterMark: config.demux.backpressure.highWaterMark,\n // },\n // videoDecode: config.decode.video, // DEPRECATED: Removed - replaced by VideoWindowDecodeSession\n videoExport: {\n compose: {\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n fps: targetFps,\n backgroundColor: '#000000',\n enableSmoothing: true,\n enableHardwareAcceleration: true,\n fonts: config.global.fonts,\n },\n encode: {\n codec: 'avc1.4D0029',\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n bitrate: config.encode.video.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),\n framerate: targetFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n keyFrameInterval: config.encode.video.keyIntervalS\n ? Math.round(config.encode.video.keyIntervalS * targetFps)\n : targetFps,\n ...config.encode.video,\n },\n },\n audioCompose: {\n enableDucking: config.compose.audio.enableDucking,\n },\n };\n }\n\n /**\n * Calculate default video bitrate based on resolution\n * Optimized for social media platforms (YouTube, TikTok, WeChat, etc.)\n */\n private calculateDefaultBitrate(width: number, height: number): number {\n const pixels = width * height;\n\n // Bitrate recommendations for H.264 Main Profile VBR:\n // - 720p (921,600 px): 5 Mbps\n // - 1080p (2,073,600 px): 8 Mbps\n // - 4K (8,294,400 px): 25 Mbps\n if (pixels <= 921_600) {\n // 720p and below\n return 5_000_000;\n } else if (pixels <= 2_073_600) {\n // 1080p\n return 8_000_000;\n } else if (pixels <= 3_686_400) {\n // 1440p (2K)\n return 16_000_000;\n } else {\n // 4K and above\n return 25_000_000;\n }\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob | null> {\n return this.exportScheduler.execute(model, options);\n }\n\n /**\n * Preheat a specific clip's window range\n * Used by PlaybackController for cross-clip window preheating\n *\n * @param clipId - Clip identifier\n * @param clipRelativeStart - Start time relative to clip (microseconds), 0 = clip start\n * @param clipRelativeEnd - End time relative to clip (microseconds)\n * @param globalTimeUs - Global timeline position (for globalTimeUs in cache)\n */\n async preheatClipWindow(\n clipId: string,\n clipRelativeStart: TimeUs,\n clipRelativeEnd: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<void> {\n if (!this.compositionModel) return;\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !hasResourceId(clip)) return;\n\n const resourceId = clip.resourceId;\n const resource = this.compositionModel.getResource(resourceId);\n // Terminal error: do not retry preload/preheat.\n if (resource?.state === 'error' && resource.error?.terminal === true) {\n return;\n }\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n\n const resourceStart = trimStartUs + clipRelativeStart * playbackRate;\n const resourceEnd = trimStartUs + clipRelativeEnd * playbackRate;\n\n // Ensure resource is downloaded\n try {\n await this.resourceLoader.load(resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n } catch (error) {\n if (isTerminalMediaResourceError(error)) {\n return;\n }\n throw error;\n }\n\n // Create temporary on-demand session for this window\n const session = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId,\n resourceUri: resource?.uri ?? '',\n targetTimeUs: resourceStart,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n // Decode the entire window range for this clip (using resource time)\n await session.decodeWindow(resourceStart, resourceEnd);\n } catch (error) {\n console.warn(`[Orchestrator] Preheat clip ${clipId} window failed:`, error);\n // Non-critical, don't throw\n } finally {\n await session.dispose();\n }\n }\n\n /**\n * Get render state for real-time composition\n * Returns layers ready for VideoComposer\n */\n async getRenderState(\n timeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<{ layers: any[]; transition?: any } | null> {\n const startModelToken = this.modelToken;\n const startModel = this.compositionModel;\n if (!startModel) {\n return null;\n }\n\n // Ensure frame/resource is ready (this populates L1 if needed)\n const frame = await this.getFrame(timeUs, options);\n\n // Model may switch while awaiting; bail out to avoid mixing old clip/instructions with new model.\n if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {\n return null;\n }\n\n // In probe mode, return null to trigger buffering on the caller side.\n if (!frame) {\n return null;\n }\n\n const clip = startModel.getClipsAtTime(timeUs, startModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n let resourceTimeUs = trimStartUs + clipRelativeTimeUs * playbackRate;\n if (clip.durationUs > 0) {\n const resourceSpanUs = clip.durationUs * playbackRate;\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + resourceSpanUs - 1);\n }\n\n // Get instructions from planner\n const instructions = this.planner.getInstructions(clip.id);\n if (!instructions) {\n return null;\n }\n\n // Build layers array\n const layers: any[] = [];\n\n // 1. Filter active layers at this timestamp (uses clip-relative time for activeRanges)\n const activeLayers = instructions.layers.filter((layer: any) => {\n if (!layer.payload.attachmentId) {\n // Main track layer is always active\n return true;\n }\n if (layer.status !== 'ready') {\n return false;\n }\n // Check if layer is active at current timestamp\n return layer.activeRanges.some(\n (range: any) => clipRelativeTimeUs >= range.startUs && clipRelativeTimeUs < range.endUs\n );\n });\n\n // 2. Materialize layers\n for (const layerPlan of activeLayers) {\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n layers.push({\n id: layerPlan.layerId,\n type: 'video',\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n rcFrame: frame,\n });\n continue;\n }\n\n const layer = await this.materializeLayer(layerPlan, clip, resourceTimeUs, timeUs);\n if (layer) {\n layers.push(layer);\n }\n }\n\n return { layers };\n }\n\n /**\n * Materialize a serialized layer plan into concrete Layer\n */\n private async materializeLayer(\n layerPlan: any,\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<any | null> {\n const baseLayer: any = {\n id: layerPlan.layerId,\n type: layerPlan.type,\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n };\n\n // Video layer - fetch raw VideoFrame from L1 (RcFrame wrapper)\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n const rcFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (!rcFrame) {\n console.warn('[Orchestrator] Video frame not found in L1:', clip.id, resourceTimeUs);\n return null;\n }\n\n return {\n ...baseLayer,\n type: 'video',\n rcFrame: rcFrame,\n };\n }\n\n // Text layer\n if (layerPlan.type === 'text') {\n const payload = layerPlan.payload;\n return {\n ...baseLayer,\n type: 'text',\n text: payload.text,\n localeCode: payload.localeCode,\n fontConfig: payload.fontConfig,\n animation: payload.animation,\n wordTimings: payload.wordTimings,\n letterCase: payload.letterCase,\n };\n }\n\n // Image layer\n if (layerPlan.type === 'image') {\n const payload = layerPlan.payload;\n const resource = this.compositionModel?.getResource(payload.resourceId);\n if (!resource) {\n return null;\n }\n\n const source = await this.resourceLoader.loadImage(resource);\n const imageLayer: any = {\n ...baseLayer,\n type: 'image',\n source,\n attachmentId: payload.attachmentId,\n };\n\n // Add renderConfig if valid\n const filteredRenderConfig = filterRenderConfig(\n payload.renderConfig,\n `image layer ${payload.attachmentId || layerPlan.layerId}`\n );\n if (filteredRenderConfig) {\n imageLayer.renderConfig = filteredRenderConfig;\n }\n\n // Handle animation (overlays)\n if (payload.animation) {\n const { position, keyframes, overlayClipStartUs } = payload.animation;\n\n // Calculate time relative to overlay clip start\n const relativeTimeUs = globalTimeUs - overlayClipStartUs;\n\n // If outside keyframe range, hide\n if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {\n return null; // Not visible at this time\n }\n\n const rotationRad = 0; // TODO: interpolate from keyframes\n\n imageLayer.transform = {\n x: position.x,\n y: position.y,\n scaleX: 1,\n scaleY: 1,\n rotation: rotationRad,\n anchorX: 0.5,\n anchorY: 0.5,\n };\n }\n\n return imageLayer;\n }\n\n return baseLayer;\n }\n}\n"],"names":["applyModelPatch"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Orchestrator.js","sources":["../../src/orchestrator/Orchestrator.ts"],"sourcesContent":["import { EventBus } from '../event/EventBus';\nimport { WorkerPool } from '../worker/WorkerPool';\nimport { applyPatch as applyModelPatch } from '../model/patch';\nimport { ResourceLoader } from '../stages/load/ResourceLoader';\nimport { CacheManager } from '../cache/CacheManager';\nimport { ConfigLoader } from '../config/ConfigLoader';\nimport type { IOrchestrator, OrchestratorConfig, RenderFrameOptions } from './types';\nimport { WorkerType } from '../worker/types';\nimport { CompositionModel, CompositionPatch, Resource, TimeUs, RcFrame, Clip } from '../model';\nimport { hasResourceId, videoClipPlaybackRate } from '../model/types';\nimport { MeframeEvent, type EventPayloadMap } from '../event/events';\nimport { CompositionPlanner } from './CompositionPlanner';\nimport { AudioWindowPreparer } from './AudioWindowPreparer';\nimport { AudioPreviewSession } from './AudioPreviewSession';\nimport { AudioExportSession } from './AudioExportSession';\nimport { MuxManager } from '../stages/mux/MuxManager';\nimport { ExportOptions } from '../types';\nimport { VideoWindowDecodeSession } from './VideoWindowDecodeSession';\nimport { ExportScheduler } from './ExportScheduler';\nimport { filterRenderConfig } from '../utils/object-utils';\nimport { isTerminalMediaResourceError } from '../utils/errors';\nimport { LoadProgress } from '@/stages/load/types';\nimport { attachmentEntranceRelativeTimeUs } from '../stages/compose/text-layer-animation-time';\n\nexport class Orchestrator implements IOrchestrator {\n workers: WorkerPool;\n eventBus: EventBus<EventPayloadMap>;\n compositionModel: CompositionModel | null = null;\n resourceLoader: ResourceLoader;\n cacheManager: CacheManager;\n planner: CompositionPlanner;\n audio: {\n prepare: AudioWindowPreparer;\n preview: AudioPreviewSession;\n export: AudioExportSession;\n };\n muxManager: MuxManager;\n exportScheduler: ExportScheduler;\n\n private isInitialized = false;\n private config = ConfigLoader.getInstance().getConfig();\n private ensureCacheDebounceTimer: number | null = null;\n private activeDecodeSession: VideoWindowDecodeSession | null = null;\n private modelToken = 0;\n readonly events: Pick<EventBus<EventPayloadMap>, 'on' | 'off' | 'once'>;\n\n constructor(config: OrchestratorConfig) {\n // Use provided eventBus or create a new one\n this.eventBus = config.eventBus || new EventBus<EventPayloadMap>();\n this.events = this.eventBus.asReadonly();\n\n // Initialize config first\n this.config = ConfigLoader.getInstance().getConfig();\n\n const workerConfigs = this.buildWorkerConfigs();\n\n // Initialize WorkerPool with worker path from config\n this.workers = new WorkerPool({\n eventBus: this.eventBus,\n workerConfigs,\n workerPath: config.workerPath,\n workerExtension: config.workerExtension,\n });\n\n const maxMemoryMB = config.cacheConfig?.l1Size || this.config.cache?.l1?.maxMemoryMB || 1024;\n const maxGOPs = this.config.decode?.video?.maxGOPs || 4;\n\n this.cacheManager = new CacheManager(\n {\n l1: {\n maxMemoryMB,\n maxGOPs,\n },\n resource: {\n projectId: config.projectId || this.config.global.projectId || 'default',\n },\n },\n this.eventBus\n );\n\n this.resourceLoader = new ResourceLoader({\n cacheManager: this.cacheManager,\n eventBus: this.eventBus,\n config: {\n maxConcurrent: this.config.load.maxConcurrent,\n preloadConcurrency: 2, // Fixed preload concurrency for idle background loading\n },\n onProgress: (progress) => this.handleResourceProgress(progress),\n onStateChange: (resourceId, state) => this.handleResourceStateChange(resourceId, state),\n });\n\n this.planner = new CompositionPlanner();\n\n const audioPreparer = new AudioWindowPreparer({\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n eventBus: this.eventBus,\n });\n\n const audioPreview = new AudioPreviewSession({\n cacheManager: this.cacheManager,\n preparer: audioPreparer,\n });\n\n const audioExport = new AudioExportSession({\n cacheManager: this.cacheManager,\n preparer: audioPreparer,\n });\n\n this.audio = { prepare: audioPreparer, preview: audioPreview, export: audioExport };\n\n this.muxManager = new MuxManager();\n\n this.exportScheduler = new ExportScheduler({\n workerPool: this.workers,\n planner: this.planner,\n cacheManager: this.cacheManager,\n resourceLoader: this.resourceLoader,\n muxManager: this.muxManager,\n audioSession: this.audio.export,\n workerConfigsProvider: () => this.buildWorkerConfigs(),\n eventBus: this.eventBus,\n });\n\n this.setupResourceFirstFrameHandler();\n }\n\n private setupResourceFirstFrameHandler(): void {\n this.eventBus.on(MeframeEvent.ResourceFirstFrameReady, async (payload) => {\n const { resourceId, clipId, index, chunks } = payload;\n\n if (!this.compositionModel) return;\n\n // Find the specific clip\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !clip.trackId) return;\n\n // Only decode first frame for clips that start at composition time 0\n // (these clips need the resource's first frame as cover)\n if (clip.startUs === 0) {\n const fps = this.compositionModel.fps ?? 30;\n await VideoWindowDecodeSession.decodeAndCacheFirstFrame(\n resourceId,\n chunks,\n index,\n clip,\n this.cacheManager,\n fps\n );\n }\n });\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n await this.cacheManager.init();\n\n this.isInitialized = true;\n }\n\n // Event methods - forward to eventBus\n on<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.on(event, handler);\n }\n\n off<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.off(event, handler);\n }\n\n once<K extends keyof EventPayloadMap>(\n event: K,\n handler: (payload: EventPayloadMap[K]) => void\n ): void {\n this.eventBus.once(event, handler);\n }\n\n cancelActiveDecoding(): void {\n if (this.activeDecodeSession) {\n void this.activeDecodeSession.dispose();\n this.activeDecodeSession = null;\n }\n }\n\n async setCompositionModel(model: CompositionModel): Promise<void> {\n this.cacheManager.clear();\n this.cancelActiveDecoding();\n this.audio.preview.stopPlayback();\n this.audio.preview.invalidatePreviewMixCache();\n this.modelToken++;\n this.compositionModel = model;\n this.planner.setModel(model);\n\n // IMPORTANT: avoid a window where audio.prepare still references the old model while\n // resourceLoader already switched (resourceLoader.setModel sets this.model before awaiting).\n // This matters when the host app triggers play/seek without awaiting setCompositionModel().\n const resourceLoaderSetModelPromise = this.resourceLoader.setModel(model);\n this.audio.prepare.setModel(model);\n // ensure the cover resource is preloaded before preview starts\n await resourceLoaderSetModelPromise;\n\n this.eventBus.emit(MeframeEvent.ModelSet, model);\n\n this.eventBus.emit(MeframeEvent.CompositionUpdated, {\n trackCount: model.tracks.length,\n clipCount: model.tracks.reduce((acc: number, track: any) => acc + track.clips.length, 0),\n durationUs: model.durationUs,\n });\n }\n\n async applyPatch(patch: CompositionPatch): Promise<void> {\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n // Apply patch and get affected clip IDs (simplified for 2-Clip strategy)\n // Note: addTrack/removeTrack already call buildIndexes() in patch.ts\n const affectedClipIds = applyModelPatch(this.compositionModel, patch);\n this.planner.applyPatch(patch, affectedClipIds);\n this.eventBus.emit(MeframeEvent.PatchApplied, {\n operations: patch.operations.length,\n affectedClips: Array.from(affectedClipIds),\n });\n\n // Process clip updates\n for (const clipId of affectedClipIds) {\n this.cacheManager.invalidateClip(clipId);\n }\n\n // Reactivate updated audio clips\n const reactivatedAudioClips: string[] = [];\n const reactivatedVideoClips: string[] = [];\n for (const clipId of affectedClipIds) {\n const clip = this.compositionModel.findClip(clipId);\n if (clip?.trackKind === 'audio') {\n await this.audio.prepare.deactivateClip(clipId);\n reactivatedAudioClips.push(clipId);\n } else if (clip?.trackKind === 'video') {\n reactivatedVideoClips.push(clipId);\n }\n }\n\n // Activate all audio clips (including reactivated ones)\n await this.audio.prepare.activateAllAudioClips();\n\n // Note: No need to restart per-clip playback in new architecture\n // scheduleAudio() uses OfflineAudioMixer which automatically includes all active clips\n }\n\n private handleResourceProgress(progress: LoadProgress): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.eventBus.emit(MeframeEvent.LoadProgress, {\n resourceId: progress.resourceId,\n cached: progress.cached,\n loadedBytes: progress.bytesLoaded,\n total: progress.totalBytes,\n percentage: progress.percentage,\n speed: progress.speed > 0 ? progress.speed : 128000,\n estimatedTimeRemaining: progress.estimatedTimeRemaining,\n });\n }\n\n private handleResourceStateChange(resourceId: string, state: Resource['state']): void {\n if (!this.compositionModel) {\n return;\n }\n\n this.compositionModel.updateResourceState(resourceId, state ?? 'pending');\n\n if (state !== 'ready') {\n return;\n }\n\n // For preview, simple cache invalidation or triggering re-render might be enough\n // if we were caching instructions. But PlaybackController pulls instructions every frame.\n // So just updating Model state is enough.\n }\n\n async getFrame(timeUs: TimeUs, options?: RenderFrameOptions): Promise<RcFrame | null> {\n const signal = options?.signal;\n const preheat = options?.preheat ?? false;\n const scrub = options?.scrub ?? false;\n const mode = options?.mode ?? 'blocking';\n\n if (!this.compositionModel) {\n throw new Error('No composition model set');\n }\n\n const clip = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n\n // Map timeline position within clip to resource timestamp (decoder / cache coordinates).\n const clipTimelineUs = options?.relativeTimeUs ?? timeUs - clip.startUs;\n let resourceTimeUs = trimStartUs + clipTimelineUs * playbackRate;\n\n const resourceSpanUs = clip.durationUs * playbackRate;\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + Math.max(0, resourceSpanUs) - 1);\n\n // 1. Check L1 window cache\n // Note: preheat mode skips cache check to force decoding the entire window\n // Why: Video cache is frame-level (point query), not window-level (range query)\n // A single cached frame doesn't guarantee the entire window is cached\n // Without preheat flag, preheating would return early on first frame hit\n if (!preheat) {\n const cachedFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (cachedFrame) {\n // Scrubbing prefers a true frame near the target. A cached keyframe preview can be far away\n // (VideoL1Cache returns nearest by timestamp), which makes dragging feel \"stuck on I-frames\".\n if (scrub) {\n const fps = this.compositionModel?.fps ?? 30;\n const maxDistanceUs = Math.max(1, Math.round(1_000_000 / fps));\n const cachedTs = cachedFrame.timestampUs ?? 0;\n if (Math.abs(cachedTs - resourceTimeUs) <= maxDistanceUs) {\n return cachedFrame;\n }\n } else {\n return cachedFrame;\n }\n }\n }\n\n if (signal?.aborted) {\n return null;\n }\n\n // 2. Try decode from OPFS resource (on-demand path)\n const resourceFrame = await this.decodeFromResource(clip, resourceTimeUs, timeUs, {\n ...options,\n mode,\n });\n return resourceFrame;\n }\n\n /**\n * Compose frame from OPFS resource (on-demand decoding)\n * This is the new path for long clips with window caching\n *\n * @param clip - The clip to decode\n * @param resourceTimeUs - Time in resource-relative coordinates (same as frame.timestamp)\n * @param globalTimeUs - Time in composition timeline\n */\n private async decodeFromResource(\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<RcFrame | null> {\n if (!hasResourceId(clip)) return null;\n\n const resourceId = clip.resourceId;\n const trimStartUs = clip.trimStartUs ?? 0;\n const startModelToken = this.modelToken;\n const startModel = this.compositionModel;\n\n // Check resource state\n const resource = this.compositionModel?.getResource(resourceId);\n const isReady = resource?.state === 'ready';\n const isTerminalError = resource?.state === 'error' && resource.error?.terminal === true;\n\n const fetchOptions = {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n };\n\n const mode = options?.mode ?? 'blocking';\n\n // If the resource is already in an error terminal state, do not retry loading.\n // Returning null here prevents playback start from failing and avoids repeated logs.\n if (isTerminalError) {\n return null;\n }\n\n // In probe mode, if not ready, trigger fetch in background and return null.\n // Note: If resource is already ready, probe mode behaves the same as blocking mode\n // (it may still decode from OPFS). The only guaranteed non-blocking part is resource loading.\n if (mode === 'probe' && !isReady) {\n void this.resourceLoader.load(resourceId, fetchOptions).catch(() => {});\n return null;\n }\n\n // Normal mode: wait for download\n try {\n await this.resourceLoader.load(resourceId, fetchOptions);\n } catch (error) {\n // Permanent media/container mismatch should not crash playback loops.\n // Treat as \"no frame available\" and let upper layers decide presentation.\n if (isTerminalMediaResourceError(error)) {\n console.warn('[Orchestrator] Resource not decodable, returning null frame:', {\n clipId: clip.id,\n resourceId,\n error: { name: error.name, message: error.message },\n });\n return null;\n }\n throw error;\n }\n\n if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {\n console.warn('[Orchestrator] Model switched during decodeFromResource:', {\n startModelToken,\n currentModelToken: this.modelToken,\n clipId: clip.id,\n resourceId,\n globalTimeUs,\n resourceTimeUs,\n mode,\n });\n return null;\n }\n\n this.cancelActiveDecoding();\n\n // Create temporary on-demand video session\n const session = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId,\n resourceUri: resource?.uri ?? '',\n targetTimeUs: resourceTimeUs,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel!,\n fps: this.compositionModel?.fps ?? 30,\n });\n\n this.activeDecodeSession = session;\n\n try {\n if (options?.scrub) {\n await session.decodeScrub(resourceTimeUs);\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n }\n\n // Decode window: from target position to target + 3s\n // Window bounds are in resource time: [trimStartUs, trimStartUs + durationUs)\n const DECODE_WINDOW_SIZE = 3_000_000;\n\n const playbackRate = videoClipPlaybackRate(clip);\n const resourceSpanUs = clip.durationUs * playbackRate;\n const windowStart = resourceTimeUs;\n const windowEnd = Math.min(trimStartUs + resourceSpanUs, resourceTimeUs + DECODE_WINDOW_SIZE);\n\n await session.decodeWindow(windowStart, windowEnd);\n // Return target frame from L1 cache\n return this.cacheManager.getFrame(resourceTimeUs, clip.id);\n } catch (error) {\n if (session.isDisposed) {\n return null;\n }\n console.error('[Orchestrator] Error composing from resource:', error);\n return null;\n } finally {\n if (this.activeDecodeSession === session) {\n this.activeDecodeSession = null;\n }\n await session.dispose();\n }\n }\n\n /**\n * Wait for clip cache to be ready for playback\n * Returns true if minimum cache is ready, false if timeout\n */\n async waitForClipReady(\n timeUs: TimeUs,\n options?: { minFrameCount?: number; timeoutMs?: number }\n ): Promise<boolean> {\n if (!this.compositionModel) {\n return false;\n }\n\n const clips = this.compositionModel.getClipsAtTime(timeUs, this.compositionModel.mainTrackId);\n if (clips.length === 0) {\n return true;\n }\n\n const currentClip = clips[0];\n if (!currentClip) {\n return true;\n }\n\n return this.cacheManager.waitForClipReady(currentClip.id, {\n minFrameCount: options?.minFrameCount ?? 5,\n timeoutMs: options?.timeoutMs ?? 5_000,\n });\n }\n\n async dispose(): Promise<void> {\n if (this.ensureCacheDebounceTimer !== null) {\n clearTimeout(this.ensureCacheDebounceTimer);\n this.ensureCacheDebounceTimer = null;\n }\n\n this.resourceLoader.dispose();\n await this.cacheManager.clear();\n\n this.workers.terminateAll();\n this.compositionModel = null;\n this.eventBus.dispose();\n }\n\n private buildWorkerConfigs(): Record<WorkerType, any> {\n const config = this.config;\n const defaultCanvasWidth = config.global.defaultCanvasWidth;\n const defaultCanvasHeight = config.global.defaultCanvasHeight;\n const defaultFps = config.global.defaultFps;\n\n const targetFps = this.compositionModel?.fps ?? defaultFps;\n\n return {\n // videoDemux: { // DEPRECATED: Removed - replaced by IndexedVideoSource\n // highWaterMark: config.demux.backpressure.highWaterMark,\n // },\n // videoDecode: config.decode.video, // DEPRECATED: Removed - replaced by VideoWindowDecodeSession\n videoExport: {\n compose: {\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n fps: targetFps,\n backgroundColor: '#000000',\n enableSmoothing: true,\n enableHardwareAcceleration: true,\n fonts: config.global.fonts,\n },\n encode: {\n codec: 'avc1.4D0029',\n width: defaultCanvasWidth,\n height: defaultCanvasHeight,\n bitrate: config.encode.video.bitrateKbps\n ? config.encode.video.bitrateKbps * 1000\n : this.calculateDefaultBitrate(defaultCanvasWidth, defaultCanvasHeight),\n framerate: targetFps,\n latencyMode: 'quality',\n bitrateMode: 'variable',\n hardwareAcceleration: 'no-preference',\n keyFrameInterval: config.encode.video.keyIntervalS\n ? Math.round(config.encode.video.keyIntervalS * targetFps)\n : targetFps,\n ...config.encode.video,\n },\n },\n audioCompose: {\n enableDucking: config.compose.audio.enableDucking,\n },\n };\n }\n\n /**\n * Calculate default video bitrate based on resolution\n * Optimized for social media platforms (YouTube, TikTok, WeChat, etc.)\n */\n private calculateDefaultBitrate(width: number, height: number): number {\n const pixels = width * height;\n\n // Bitrate recommendations for H.264 Main Profile VBR:\n // - 720p (921,600 px): 5 Mbps\n // - 1080p (2,073,600 px): 8 Mbps\n // - 4K (8,294,400 px): 25 Mbps\n if (pixels <= 921_600) {\n // 720p and below\n return 5_000_000;\n } else if (pixels <= 2_073_600) {\n // 1080p\n return 8_000_000;\n } else if (pixels <= 3_686_400) {\n // 1440p (2K)\n return 16_000_000;\n } else {\n // 4K and above\n return 25_000_000;\n }\n }\n\n async export(model: CompositionModel, options: ExportOptions): Promise<Blob | null> {\n return this.exportScheduler.execute(model, options);\n }\n\n /**\n * Preheat a specific clip's window range\n * Used by PlaybackController for cross-clip window preheating\n *\n * @param clipId - Clip identifier\n * @param clipRelativeStart - Start time relative to clip (microseconds), 0 = clip start\n * @param clipRelativeEnd - End time relative to clip (microseconds)\n * @param globalTimeUs - Global timeline position (for globalTimeUs in cache)\n */\n async preheatClipWindow(\n clipId: string,\n clipRelativeStart: TimeUs,\n clipRelativeEnd: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<void> {\n if (!this.compositionModel) return;\n\n const clip = this.compositionModel.findClip(clipId);\n if (!clip || !hasResourceId(clip)) return;\n\n const resourceId = clip.resourceId;\n const resource = this.compositionModel.getResource(resourceId);\n // Terminal error: do not retry preload/preheat.\n if (resource?.state === 'error' && resource.error?.terminal === true) {\n return;\n }\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n\n const resourceStart = trimStartUs + clipRelativeStart * playbackRate;\n const resourceEnd = trimStartUs + clipRelativeEnd * playbackRate;\n\n // Ensure resource is downloaded\n try {\n await this.resourceLoader.load(resourceId, {\n isPreload: false,\n clipId: clip.id,\n trackId: clip.trackId,\n });\n } catch (error) {\n if (isTerminalMediaResourceError(error)) {\n return;\n }\n throw error;\n }\n\n // Create temporary on-demand session for this window\n const session = await VideoWindowDecodeSession.create({\n clipId: clip.id,\n resourceId,\n resourceUri: resource?.uri ?? '',\n targetTimeUs: resourceStart,\n globalTimeUs,\n resourceLoader: this.resourceLoader,\n mp4IndexCache: this.cacheManager.mp4IndexCache,\n cacheManager: this.cacheManager,\n compositionModel: this.compositionModel,\n fps: this.compositionModel.fps ?? 30,\n });\n\n try {\n // Decode the entire window range for this clip (using resource time)\n await session.decodeWindow(resourceStart, resourceEnd);\n } catch (error) {\n console.warn(`[Orchestrator] Preheat clip ${clipId} window failed:`, error);\n // Non-critical, don't throw\n } finally {\n await session.dispose();\n }\n }\n\n /**\n * Get render state for real-time composition\n * Returns layers ready for VideoComposer\n */\n async getRenderState(\n timeUs: TimeUs,\n options?: RenderFrameOptions\n ): Promise<{ layers: any[]; transition?: any } | null> {\n const startModelToken = this.modelToken;\n const startModel = this.compositionModel;\n if (!startModel) {\n return null;\n }\n\n // Ensure frame/resource is ready (this populates L1 if needed)\n const frame = await this.getFrame(timeUs, options);\n\n // Model may switch while awaiting; bail out to avoid mixing old clip/instructions with new model.\n if (this.modelToken !== startModelToken || this.compositionModel !== startModel) {\n return null;\n }\n\n // In probe mode, return null to trigger buffering on the caller side.\n if (!frame) {\n return null;\n }\n\n const clip = startModel.getClipsAtTime(timeUs, startModel.mainTrackId)[0];\n if (!clip) {\n return null;\n }\n\n const clipRelativeTimeUs = timeUs - clip.startUs;\n const trimStartUs = clip.trimStartUs ?? 0;\n const playbackRate = videoClipPlaybackRate(clip);\n let resourceTimeUs = trimStartUs + clipRelativeTimeUs * playbackRate;\n if (clip.durationUs > 0) {\n const resourceSpanUs = clip.durationUs * playbackRate;\n resourceTimeUs = Math.min(resourceTimeUs, trimStartUs + resourceSpanUs - 1);\n }\n\n // Get instructions from planner\n const instructions = this.planner.getInstructions(clip.id);\n if (!instructions) {\n return null;\n }\n\n // Build layers array\n const layers: any[] = [];\n\n // 1. Filter active layers at this timestamp (uses clip-relative time for activeRanges)\n const activeLayers = instructions.layers.filter((layer: any) => {\n if (!layer.payload.attachmentId) {\n // Main track layer is always active\n return true;\n }\n if (layer.status !== 'ready') {\n return false;\n }\n // Check if layer is active at current timestamp\n return layer.activeRanges.some(\n (range: any) => clipRelativeTimeUs >= range.startUs && clipRelativeTimeUs < range.endUs\n );\n });\n\n // 2. Materialize layers\n for (const layerPlan of activeLayers) {\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n layers.push({\n id: layerPlan.layerId,\n type: 'video',\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n rcFrame: frame,\n });\n continue;\n }\n\n const layer = await this.materializeLayer(layerPlan, clip, resourceTimeUs, timeUs);\n if (layer) {\n layers.push(layer);\n }\n }\n\n return { layers };\n }\n\n /**\n * Materialize a serialized layer plan into concrete Layer\n */\n private async materializeLayer(\n layerPlan: any,\n clip: Clip,\n resourceTimeUs: TimeUs,\n globalTimeUs: TimeUs\n ): Promise<any | null> {\n const baseLayer: any = {\n id: layerPlan.layerId,\n type: layerPlan.type,\n zIndex: layerPlan.zIndex ?? 0,\n visible: true,\n opacity: layerPlan.opacity ?? 1,\n };\n\n // Video layer - fetch raw VideoFrame from L1 (RcFrame wrapper)\n if (layerPlan.type === 'video' && !layerPlan.payload.attachmentId) {\n const rcFrame = this.cacheManager.getFrame(resourceTimeUs, clip.id);\n if (!rcFrame) {\n console.warn('[Orchestrator] Video frame not found in L1:', clip.id, resourceTimeUs);\n return null;\n }\n\n return {\n ...baseLayer,\n type: 'video',\n rcFrame: rcFrame,\n };\n }\n\n // Text layer\n if (layerPlan.type === 'text') {\n const payload = layerPlan.payload;\n const clipRelativeTimeUs = globalTimeUs - clip.startUs;\n const entranceRelativeTimeUs = attachmentEntranceRelativeTimeUs(\n clipRelativeTimeUs,\n layerPlan.activeRanges\n );\n return {\n ...baseLayer,\n type: 'text',\n text: payload.text,\n localeCode: payload.localeCode,\n fontConfig: payload.fontConfig,\n animation: payload.animation,\n wordTimings: payload.wordTimings,\n letterCase: payload.letterCase,\n entranceRelativeTimeUs,\n };\n }\n\n // Image layer\n if (layerPlan.type === 'image') {\n const payload = layerPlan.payload;\n const resource = this.compositionModel?.getResource(payload.resourceId);\n if (!resource) {\n return null;\n }\n\n const source = await this.resourceLoader.loadImage(resource);\n const imageLayer: any = {\n ...baseLayer,\n type: 'image',\n source,\n attachmentId: payload.attachmentId,\n };\n\n // Add renderConfig if valid\n const filteredRenderConfig = filterRenderConfig(\n payload.renderConfig,\n `image layer ${payload.attachmentId || layerPlan.layerId}`\n );\n if (filteredRenderConfig) {\n imageLayer.renderConfig = filteredRenderConfig;\n }\n\n // Handle animation (overlays)\n if (payload.animation) {\n const { position, keyframes, overlayClipStartUs } = payload.animation;\n\n // Calculate time relative to overlay clip start\n const relativeTimeUs = globalTimeUs - overlayClipStartUs;\n\n // If outside keyframe range, hide\n if (relativeTimeUs < 0 || relativeTimeUs > keyframes[keyframes.length - 1].time) {\n return null; // Not visible at this time\n }\n\n const rotationRad = 0; // TODO: interpolate from keyframes\n\n imageLayer.transform = {\n x: position.x,\n y: position.y,\n scaleX: 1,\n scaleY: 1,\n rotation: rotationRad,\n anchorX: 0.5,\n anchorY: 0.5,\n };\n }\n\n return imageLayer;\n }\n\n return baseLayer;\n }\n}\n"],"names":["applyModelPatch"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"LayerRenderer.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/LayerRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAA8D,MAAM,SAAS,CAAC;AAQjG;;;GAGG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAA+D;IAC1E,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,GAAG,CAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAO;gBAGpC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAW;IASlB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIpC,OAAO,CAAC,0BAA0B;IAKlC;;OAEG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAkD/B,OAAO,CAAC,cAAc;IAqBtB;;;OAGG;IACH,OAAO,CAAC,6BAA6B;IA8BrC,OAAO,CAAC,kBAAkB;IAyC1B;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;IAkCjC,OAAO,CAAC,cAAc;IA2BtB,OAAO,CAAC,gBAAgB;IAoDxB,OAAO,CAAC,gBAAgB;IA2ExB,OAAO,CAAC,eAAe;IAgDvB,OAAO,CAAC,SAAS;IAkBjB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAKtD"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { textAnimationFrameForTextLayer } from "./text-layer-animation-time.js";
|
|
2
|
+
import { renderBasicText } from "./text-renderers/basic-text-renderer.js";
|
|
3
|
+
import { renderCaptionStaggerEntrance } from "./text-renderers/caption-stagger-entrance-renderer.js";
|
|
2
4
|
import { renderWordByWord } from "./text-renderers/word-by-word-renderer.js";
|
|
3
5
|
import { renderCharacterKTV } from "./text-renderers/character-ktv-renderer.js";
|
|
4
6
|
import { renderWordByWordFancy } from "./text-renderers/word-fancy-renderer.js";
|
|
@@ -263,6 +265,7 @@ class LayerRenderer {
|
|
|
263
265
|
renderTextLayer(layer) {
|
|
264
266
|
const animationType = layer.animation?.type;
|
|
265
267
|
const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;
|
|
268
|
+
const animFrame = textAnimationFrameForTextLayer(layer, this.currentFrame, this.fps);
|
|
266
269
|
const needsWordTimings = ["wordByWord", "characterKTV", "wordByWordFancy"].includes(
|
|
267
270
|
animationType || ""
|
|
268
271
|
);
|
|
@@ -272,24 +275,32 @@ class LayerRenderer {
|
|
|
272
275
|
}
|
|
273
276
|
switch (animationType) {
|
|
274
277
|
case "wordByWord":
|
|
275
|
-
renderWordByWord(this.ctx, layer, this.width, this.height,
|
|
278
|
+
renderWordByWord(this.ctx, layer, this.width, this.height, animFrame, this.fps);
|
|
276
279
|
break;
|
|
277
280
|
case "characterKTV":
|
|
278
|
-
renderCharacterKTV(this.ctx, layer, this.width, this.height,
|
|
281
|
+
renderCharacterKTV(this.ctx, layer, this.width, this.height, animFrame, this.fps);
|
|
279
282
|
break;
|
|
280
283
|
case "wordByWordFancy":
|
|
281
|
-
renderWordByWordFancy(
|
|
284
|
+
renderWordByWordFancy(this.ctx, layer, this.width, this.height, animFrame, this.fps);
|
|
285
|
+
break;
|
|
286
|
+
case "fade":
|
|
287
|
+
case "slideUp":
|
|
288
|
+
case "scale":
|
|
289
|
+
case "rotateScale":
|
|
290
|
+
case "blur":
|
|
291
|
+
case "flip3d":
|
|
292
|
+
case "typewriter":
|
|
293
|
+
case "letterSpread":
|
|
294
|
+
renderCaptionStaggerEntrance(
|
|
282
295
|
this.ctx,
|
|
283
296
|
layer,
|
|
284
297
|
this.width,
|
|
285
298
|
this.height,
|
|
286
|
-
|
|
287
|
-
this.fps
|
|
299
|
+
animFrame,
|
|
300
|
+
this.fps,
|
|
301
|
+
animationType
|
|
288
302
|
);
|
|
289
303
|
break;
|
|
290
|
-
case "fade":
|
|
291
|
-
renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
292
|
-
break;
|
|
293
304
|
default:
|
|
294
305
|
renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);
|
|
295
306
|
break;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\nimport { renderBasicText, renderTextWithEntrance } from './text-renderers/basic-text-renderer';\nimport { renderWordByWord } from './text-renderers/word-by-word-renderer';\nimport { renderCharacterKTV } from './text-renderers/character-ktv-renderer';\nimport { renderWordByWordFancy } from './text-renderers/word-fancy-renderer';\n\n/**\n * LayerRenderer - Handles rendering of individual layers\n * Single responsibility: Draw a single layer to the canvas context\n */\nexport class LayerRenderer {\n private ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n private width: number;\n private height: number;\n private currentFrame: number = 0;\n private fps: number = 30;\n private readonly FILL_THRESHOLD = 0.9;\n\n constructor(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n width: number,\n height: number,\n fps: number = 30\n ) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.fps = fps;\n this.ensureHighQualityRendering();\n }\n\n setCurrentFrame(frame: number): void {\n this.currentFrame = frame;\n }\n\n private ensureHighQualityRendering(): void {\n this.ctx.imageSmoothingEnabled = true;\n this.ctx.imageSmoothingQuality = 'high';\n }\n\n /**\n * Render a single layer with all its properties\n */\n renderLayer(layer: Layer): void {\n if (!layer.visible || layer.opacity <= 0) return;\n\n // Only save/restore context if layer has properties that need it\n const needsStateManagement =\n layer.opacity !== 1 || layer.blendMode || layer.transform || layer.mask;\n\n if (needsStateManagement) {\n this.ctx.save();\n }\n\n try {\n // Apply layer properties only when needed\n if (layer.opacity !== 1) {\n this.ctx.globalAlpha = layer.opacity;\n }\n\n if (layer.blendMode) {\n this.ctx.globalCompositeOperation = layer.blendMode;\n }\n\n if (layer.transform) {\n // Get layer dimensions for transform anchor calculation\n const layerDimensions = this.getLayerDimensions(layer);\n this.applyTransform(layer.transform, layerDimensions);\n }\n // Render based on layer type\n switch (layer.type) {\n case 'video':\n this.renderVideoLayer(layer as VideoLayer);\n break;\n case 'image':\n this.renderImageLayer(layer as ImageLayer);\n break;\n case 'text':\n this.renderTextLayer(layer as TextLayer);\n break;\n }\n\n // Apply mask if present\n if (layer.mask) {\n this.applyMask(layer.mask);\n }\n } finally {\n if (needsStateManagement) {\n this.ctx.restore();\n }\n }\n }\n\n private parseDimension(\n value: number | string | undefined,\n canvasSize: number\n ): number | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'number') return value;\n\n // value is string at this point\n const strValue = value as string;\n\n // Parse percentage string like \"5%\"\n if (strValue.includes('%')) {\n const numValue = parseFloat(strValue);\n return isNaN(numValue) ? undefined : (numValue / 100) * canvasSize;\n }\n\n // Parse as pixel value\n const parsed = parseFloat(strValue);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n /**\n * Calculate dimensions from renderConfig\n * Returns dimensions maintaining aspect ratio when only one dimension is specified\n */\n private calculateDimensionsFromConfig(\n sourceWidth: number,\n sourceHeight: number,\n renderConfig: { width?: number | string; height?: number | string } | undefined\n ): { width: number; height: number } {\n if (!renderConfig) {\n return { width: sourceWidth, height: sourceHeight };\n }\n\n const width = this.parseDimension(renderConfig.width, this.width);\n const height = this.parseDimension(renderConfig.height, this.height);\n\n if (width && height) {\n return { width, height };\n } else if (width) {\n return {\n width,\n height: Math.round((sourceHeight / sourceWidth) * width),\n };\n } else if (height) {\n return {\n width: Math.round((sourceWidth / sourceHeight) * height),\n height,\n };\n } else {\n // renderConfig exists but empty, use original size\n return { width: sourceWidth, height: sourceHeight };\n }\n }\n\n private getLayerDimensions(layer: Layer): { width: number; height: number } {\n if (layer.type === 'image') {\n const imageLayer = layer as ImageLayer;\n if (imageLayer.source) {\n const imgWidth = imageLayer.source.width;\n const imgHeight = imageLayer.source.height;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n imageLayer.renderConfig?.width !== undefined ||\n imageLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: imgWidth, height: imgHeight };\n }\n } else if (layer.type === 'video') {\n const videoLayer = layer as VideoLayer;\n const videoFrame = videoLayer.videoFrame;\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n videoLayer.renderConfig?.width !== undefined ||\n videoLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: videoWidth, height: videoHeight };\n }\n // Default to canvas dimensions\n return { width: this.width, height: this.height };\n }\n\n /**\n * Calculate render dimensions with smart fill logic\n * @param sourceWidth Source width\n * @param sourceHeight Source height\n * @param naturalScale Natural scale factor (scaleY for video, scaleX for image)\n * @returns Render dimensions and position\n */\n private calculateRenderDimensions(\n sourceWidth: number,\n sourceHeight: number,\n naturalScale: number\n ): { width: number; height: number; x: number; y: number } {\n const scaledWidth = sourceWidth * naturalScale;\n const scaledHeight = sourceHeight * naturalScale;\n\n // Smart fill: when scaled size is close to container (>90%), use cover mode\n const shouldFill =\n scaledWidth / this.width > this.FILL_THRESHOLD &&\n scaledHeight / this.height > this.FILL_THRESHOLD;\n\n let renderWidth: number;\n let renderHeight: number;\n\n if (shouldFill) {\n // Cover mode: use Math.max to ensure entire canvas is covered while maintaining aspect ratio\n const coverScale = Math.max(this.width / sourceWidth, this.height / sourceHeight);\n renderWidth = Math.round(sourceWidth * coverScale);\n renderHeight = Math.round(sourceHeight * coverScale);\n } else {\n // Natural scale mode: use scaled dimensions\n renderWidth = Math.round(scaledWidth);\n renderHeight = Math.round(scaledHeight);\n }\n\n // Center the content\n const renderX = Math.round((this.width - renderWidth) / 2);\n const renderY = Math.round((this.height - renderHeight) / 2);\n\n return { width: renderWidth, height: renderHeight, x: renderX, y: renderY };\n }\n\n private applyTransform(\n transform: Transform2D,\n layerDimensions: { width: number; height: number }\n ): void {\n // Use layer dimensions (not canvas dimensions) for anchor calculation\n const anchorX = transform.anchorX ?? 0.5;\n const anchorY = transform.anchorY ?? 0.5;\n const centerX = layerDimensions.width * anchorX;\n const centerY = layerDimensions.height * anchorY;\n\n // Move to the layer position + anchor offset\n this.ctx.translate(transform.x + centerX, transform.y + centerY);\n\n if (transform.rotation) {\n this.ctx.rotate(transform.rotation);\n }\n\n this.ctx.scale(transform.scaleX, transform.scaleY);\n\n if (transform.skewX || transform.skewY) {\n this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);\n }\n\n // Move back by anchor offset\n this.ctx.translate(-centerX, -centerY);\n }\n\n private renderVideoLayer(layer: VideoLayer): void {\n const { videoFrame, crop, renderConfig } = layer;\n\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the video\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n } else {\n // No valid renderConfig: legacy smart fill (height-based)\n const naturalScale = this.height / videoHeight;\n const dimensions = this.calculateRenderDimensions(videoWidth, videoHeight, naturalScale);\n renderX = dimensions.x;\n renderY = dimensions.y;\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n }\n\n if (crop) {\n this.ctx.drawImage(\n videoFrame,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);\n }\n // NOTE: Do not close videoFrame - it's managed by RcFrame wrapper\n }\n\n private renderImageLayer(layer: ImageLayer): void {\n const { source, crop, renderConfig, attachmentId } = layer;\n\n // Handle ImageData by putting it on canvas first\n if (source instanceof ImageData) {\n if (crop) {\n // For ImageData with crop, we need to extract the cropped region\n const tempCanvas = new OffscreenCanvas(crop.width, crop.height);\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(source, -crop.x, -crop.y);\n this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);\n } else {\n // Put ImageData directly\n this.ctx.putImageData(source, 0, 0);\n }\n return;\n }\n\n if (!source) return;\n\n const imgWidth = source.width;\n const imgHeight = source.height;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Images with renderConfig start at origin (for overlay positioning via transform)\n renderX = 0;\n renderY = 0;\n } else if (attachmentId) {\n // Attachment without valid renderConfig: use original size, start at origin\n renderWidth = imgWidth;\n renderHeight = imgHeight;\n renderX = 0;\n renderY = 0;\n } else {\n // No renderConfig and not attachment: legacy smart fill (width-based, main track)\n const naturalScale = this.width / imgWidth;\n const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the image\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n }\n\n if (crop) {\n this.ctx.drawImage(\n source,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(source, renderX, renderY, renderWidth, renderHeight);\n }\n }\n\n private renderTextLayer(layer: TextLayer): void {\n const animationType = layer.animation?.type;\n const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;\n\n const needsWordTimings = ['wordByWord', 'characterKTV', 'wordByWordFancy'].includes(\n animationType || ''\n );\n\n if (needsWordTimings && !hasWordTimings) {\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n return;\n }\n\n switch (animationType) {\n case 'wordByWord':\n renderWordByWord(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'characterKTV':\n renderCharacterKTV(this.ctx, layer, this.width, this.height, this.currentFrame, this.fps);\n break;\n case 'wordByWordFancy':\n renderWordByWordFancy(\n this.ctx,\n layer,\n this.width,\n this.height,\n this.currentFrame,\n this.fps\n );\n break;\n case 'fade':\n renderTextWithEntrance(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n default:\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n }\n }\n\n private applyMask(mask: MaskConfig): void {\n this.ctx.globalCompositeOperation = mask.invert ? 'source-out' : 'destination-in';\n\n if (mask.source) {\n this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);\n } else if (mask.shape === 'circle') {\n this.ctx.beginPath();\n this.ctx.arc(\n this.width / 2,\n this.height / 2,\n Math.min(this.width, this.height) / 2,\n 0,\n Math.PI * 2\n );\n this.ctx.fill();\n }\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LayerRenderer.js","sources":["../../../src/stages/compose/LayerRenderer.ts"],"sourcesContent":["import type { Layer, VideoLayer, ImageLayer, TextLayer, Transform2D, MaskConfig } from './types';\nimport { textAnimationFrameForTextLayer } from './text-layer-animation-time';\nimport { renderBasicText } from './text-renderers/basic-text-renderer';\nimport { renderCaptionStaggerEntrance } from './text-renderers/caption-stagger-entrance-renderer';\nimport { renderWordByWord } from './text-renderers/word-by-word-renderer';\nimport { renderCharacterKTV } from './text-renderers/character-ktv-renderer';\nimport { renderWordByWordFancy } from './text-renderers/word-fancy-renderer';\n\n/**\n * LayerRenderer - Handles rendering of individual layers\n * Single responsibility: Draw a single layer to the canvas context\n */\nexport class LayerRenderer {\n private ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n private width: number;\n private height: number;\n private currentFrame: number = 0;\n private fps: number = 30;\n private readonly FILL_THRESHOLD = 0.9;\n\n constructor(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n width: number,\n height: number,\n fps: number = 30\n ) {\n this.ctx = ctx;\n this.width = width;\n this.height = height;\n this.fps = fps;\n this.ensureHighQualityRendering();\n }\n\n setCurrentFrame(frame: number): void {\n this.currentFrame = frame;\n }\n\n private ensureHighQualityRendering(): void {\n this.ctx.imageSmoothingEnabled = true;\n this.ctx.imageSmoothingQuality = 'high';\n }\n\n /**\n * Render a single layer with all its properties\n */\n renderLayer(layer: Layer): void {\n if (!layer.visible || layer.opacity <= 0) return;\n\n // Only save/restore context if layer has properties that need it\n const needsStateManagement =\n layer.opacity !== 1 || layer.blendMode || layer.transform || layer.mask;\n\n if (needsStateManagement) {\n this.ctx.save();\n }\n\n try {\n // Apply layer properties only when needed\n if (layer.opacity !== 1) {\n this.ctx.globalAlpha = layer.opacity;\n }\n\n if (layer.blendMode) {\n this.ctx.globalCompositeOperation = layer.blendMode;\n }\n\n if (layer.transform) {\n // Get layer dimensions for transform anchor calculation\n const layerDimensions = this.getLayerDimensions(layer);\n this.applyTransform(layer.transform, layerDimensions);\n }\n // Render based on layer type\n switch (layer.type) {\n case 'video':\n this.renderVideoLayer(layer as VideoLayer);\n break;\n case 'image':\n this.renderImageLayer(layer as ImageLayer);\n break;\n case 'text':\n this.renderTextLayer(layer as TextLayer);\n break;\n }\n\n // Apply mask if present\n if (layer.mask) {\n this.applyMask(layer.mask);\n }\n } finally {\n if (needsStateManagement) {\n this.ctx.restore();\n }\n }\n }\n\n private parseDimension(\n value: number | string | undefined,\n canvasSize: number\n ): number | undefined {\n if (value === undefined) return undefined;\n if (typeof value === 'number') return value;\n\n // value is string at this point\n const strValue = value as string;\n\n // Parse percentage string like \"5%\"\n if (strValue.includes('%')) {\n const numValue = parseFloat(strValue);\n return isNaN(numValue) ? undefined : (numValue / 100) * canvasSize;\n }\n\n // Parse as pixel value\n const parsed = parseFloat(strValue);\n return isNaN(parsed) ? undefined : parsed;\n }\n\n /**\n * Calculate dimensions from renderConfig\n * Returns dimensions maintaining aspect ratio when only one dimension is specified\n */\n private calculateDimensionsFromConfig(\n sourceWidth: number,\n sourceHeight: number,\n renderConfig: { width?: number | string; height?: number | string } | undefined\n ): { width: number; height: number } {\n if (!renderConfig) {\n return { width: sourceWidth, height: sourceHeight };\n }\n\n const width = this.parseDimension(renderConfig.width, this.width);\n const height = this.parseDimension(renderConfig.height, this.height);\n\n if (width && height) {\n return { width, height };\n } else if (width) {\n return {\n width,\n height: Math.round((sourceHeight / sourceWidth) * width),\n };\n } else if (height) {\n return {\n width: Math.round((sourceWidth / sourceHeight) * height),\n height,\n };\n } else {\n // renderConfig exists but empty, use original size\n return { width: sourceWidth, height: sourceHeight };\n }\n }\n\n private getLayerDimensions(layer: Layer): { width: number; height: number } {\n if (layer.type === 'image') {\n const imageLayer = layer as ImageLayer;\n if (imageLayer.source) {\n const imgWidth = imageLayer.source.width;\n const imgHeight = imageLayer.source.height;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n imageLayer.renderConfig?.width !== undefined ||\n imageLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(imgWidth, imgHeight, imageLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: imgWidth, height: imgHeight };\n }\n } else if (layer.type === 'video') {\n const videoLayer = layer as VideoLayer;\n const videoFrame = videoLayer.videoFrame;\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n // Check if has valid renderConfig\n const hasValidRenderConfig = !!(\n videoLayer.renderConfig?.width !== undefined ||\n videoLayer.renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n return this.calculateDimensionsFromConfig(videoWidth, videoHeight, videoLayer.renderConfig);\n }\n // No valid renderConfig: return original dimensions\n return { width: videoWidth, height: videoHeight };\n }\n // Default to canvas dimensions\n return { width: this.width, height: this.height };\n }\n\n /**\n * Calculate render dimensions with smart fill logic\n * @param sourceWidth Source width\n * @param sourceHeight Source height\n * @param naturalScale Natural scale factor (scaleY for video, scaleX for image)\n * @returns Render dimensions and position\n */\n private calculateRenderDimensions(\n sourceWidth: number,\n sourceHeight: number,\n naturalScale: number\n ): { width: number; height: number; x: number; y: number } {\n const scaledWidth = sourceWidth * naturalScale;\n const scaledHeight = sourceHeight * naturalScale;\n\n // Smart fill: when scaled size is close to container (>90%), use cover mode\n const shouldFill =\n scaledWidth / this.width > this.FILL_THRESHOLD &&\n scaledHeight / this.height > this.FILL_THRESHOLD;\n\n let renderWidth: number;\n let renderHeight: number;\n\n if (shouldFill) {\n // Cover mode: use Math.max to ensure entire canvas is covered while maintaining aspect ratio\n const coverScale = Math.max(this.width / sourceWidth, this.height / sourceHeight);\n renderWidth = Math.round(sourceWidth * coverScale);\n renderHeight = Math.round(sourceHeight * coverScale);\n } else {\n // Natural scale mode: use scaled dimensions\n renderWidth = Math.round(scaledWidth);\n renderHeight = Math.round(scaledHeight);\n }\n\n // Center the content\n const renderX = Math.round((this.width - renderWidth) / 2);\n const renderY = Math.round((this.height - renderHeight) / 2);\n\n return { width: renderWidth, height: renderHeight, x: renderX, y: renderY };\n }\n\n private applyTransform(\n transform: Transform2D,\n layerDimensions: { width: number; height: number }\n ): void {\n // Use layer dimensions (not canvas dimensions) for anchor calculation\n const anchorX = transform.anchorX ?? 0.5;\n const anchorY = transform.anchorY ?? 0.5;\n const centerX = layerDimensions.width * anchorX;\n const centerY = layerDimensions.height * anchorY;\n\n // Move to the layer position + anchor offset\n this.ctx.translate(transform.x + centerX, transform.y + centerY);\n\n if (transform.rotation) {\n this.ctx.rotate(transform.rotation);\n }\n\n this.ctx.scale(transform.scaleX, transform.scaleY);\n\n if (transform.skewX || transform.skewY) {\n this.ctx.transform(1, transform.skewY ?? 0, transform.skewX ?? 0, 1, 0, 0);\n }\n\n // Move back by anchor offset\n this.ctx.translate(-centerX, -centerY);\n }\n\n private renderVideoLayer(layer: VideoLayer): void {\n const { videoFrame, crop, renderConfig } = layer;\n\n const videoWidth = videoFrame.displayWidth || videoFrame.codedWidth;\n const videoHeight = videoFrame.displayHeight || videoFrame.codedHeight;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(videoWidth, videoHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the video\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n } else {\n // No valid renderConfig: legacy smart fill (height-based)\n const naturalScale = this.height / videoHeight;\n const dimensions = this.calculateRenderDimensions(videoWidth, videoHeight, naturalScale);\n renderX = dimensions.x;\n renderY = dimensions.y;\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n }\n\n if (crop) {\n this.ctx.drawImage(\n videoFrame,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(videoFrame, renderX, renderY, renderWidth, renderHeight);\n }\n // NOTE: Do not close videoFrame - it's managed by RcFrame wrapper\n }\n\n private renderImageLayer(layer: ImageLayer): void {\n const { source, crop, renderConfig, attachmentId } = layer;\n\n // Handle ImageData by putting it on canvas first\n if (source instanceof ImageData) {\n if (crop) {\n // For ImageData with crop, we need to extract the cropped region\n const tempCanvas = new OffscreenCanvas(crop.width, crop.height);\n const tempCtx = tempCanvas.getContext('2d')!;\n tempCtx.putImageData(source, -crop.x, -crop.y);\n this.ctx.drawImage(tempCanvas, 0, 0, this.width, this.height);\n } else {\n // Put ImageData directly\n this.ctx.putImageData(source, 0, 0);\n }\n return;\n }\n\n if (!source) return;\n\n const imgWidth = source.width;\n const imgHeight = source.height;\n\n let renderX: number;\n let renderY: number;\n let renderWidth: number;\n let renderHeight: number;\n\n // Check if has valid renderConfig (with actual width or height values)\n const hasValidRenderConfig = !!(\n renderConfig?.width !== undefined || renderConfig?.height !== undefined\n );\n\n if (hasValidRenderConfig) {\n // Has valid renderConfig: explicit dimensions\n const dimensions = this.calculateDimensionsFromConfig(imgWidth, imgHeight, renderConfig);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Images with renderConfig start at origin (for overlay positioning via transform)\n renderX = 0;\n renderY = 0;\n } else if (attachmentId) {\n // Attachment without valid renderConfig: use original size, start at origin\n renderWidth = imgWidth;\n renderHeight = imgHeight;\n renderX = 0;\n renderY = 0;\n } else {\n // No renderConfig and not attachment: legacy smart fill (width-based, main track)\n const naturalScale = this.width / imgWidth;\n const dimensions = this.calculateRenderDimensions(imgWidth, imgHeight, naturalScale);\n renderWidth = dimensions.width;\n renderHeight = dimensions.height;\n // Center the image\n renderX = Math.round((this.width - renderWidth) / 2);\n renderY = Math.round((this.height - renderHeight) / 2);\n }\n\n if (crop) {\n this.ctx.drawImage(\n source,\n crop.x,\n crop.y,\n crop.width,\n crop.height,\n renderX,\n renderY,\n renderWidth,\n renderHeight\n );\n } else {\n this.ctx.drawImage(source, renderX, renderY, renderWidth, renderHeight);\n }\n }\n\n private renderTextLayer(layer: TextLayer): void {\n const animationType = layer.animation?.type;\n const hasWordTimings = layer.wordTimings && layer.wordTimings.length > 0;\n const animFrame = textAnimationFrameForTextLayer(layer, this.currentFrame, this.fps);\n\n const needsWordTimings = ['wordByWord', 'characterKTV', 'wordByWordFancy'].includes(\n animationType || ''\n );\n\n if (needsWordTimings && !hasWordTimings) {\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n return;\n }\n\n switch (animationType) {\n case 'wordByWord':\n renderWordByWord(this.ctx, layer, this.width, this.height, animFrame, this.fps);\n break;\n case 'characterKTV':\n renderCharacterKTV(this.ctx, layer, this.width, this.height, animFrame, this.fps);\n break;\n case 'wordByWordFancy':\n renderWordByWordFancy(this.ctx, layer, this.width, this.height, animFrame, this.fps);\n break;\n case 'fade':\n case 'slideUp':\n case 'scale':\n case 'rotateScale':\n case 'blur':\n case 'flip3d':\n case 'typewriter':\n case 'letterSpread':\n renderCaptionStaggerEntrance(\n this.ctx,\n layer,\n this.width,\n this.height,\n animFrame,\n this.fps,\n animationType\n );\n break;\n default:\n renderBasicText(this.ctx, layer, this.width, this.height, this.currentFrame);\n break;\n }\n }\n\n private applyMask(mask: MaskConfig): void {\n this.ctx.globalCompositeOperation = mask.invert ? 'source-out' : 'destination-in';\n\n if (mask.source) {\n this.ctx.drawImage(mask.source, 0, 0, this.width, this.height);\n } else if (mask.shape === 'circle') {\n this.ctx.beginPath();\n this.ctx.arc(\n this.width / 2,\n this.height / 2,\n Math.min(this.width, this.height) / 2,\n 0,\n Math.PI * 2\n );\n this.ctx.fill();\n }\n }\n\n updateDimensions(width: number, height: number): void {\n this.width = width;\n this.height = height;\n this.ensureHighQualityRendering();\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FontManager.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/font-system/FontManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,oBAAoB,EAAa,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"FontManager.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/font-system/FontManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,oBAAoB,EAAa,MAAM,SAAS,CAAC;AA0BvF,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAsC;IAEvD,aAAa,CACX,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,GACtB,oBAAoB;IAgBvB,OAAO,CAAC,gBAAgB;IAkCxB,UAAU,IAAI,IAAI;CAGnB;AAID,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,GACtB,oBAAoB,CAEtB;AAED,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
|
|
@@ -15,6 +15,10 @@ const PREFERRED_FONTS = {
|
|
|
15
15
|
"ar-SA": "dtjinbu",
|
|
16
16
|
other: "dtjinbu"
|
|
17
17
|
};
|
|
18
|
+
function quoteFontFamily(fontFamily) {
|
|
19
|
+
const escaped = fontFamily.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
20
|
+
return `"${escaped}"`;
|
|
21
|
+
}
|
|
18
22
|
class FontManager {
|
|
19
23
|
fontCache = /* @__PURE__ */ new Map();
|
|
20
24
|
getFontConfig(locale, templateName, fontFamilyName) {
|
|
@@ -32,11 +36,11 @@ class FontManager {
|
|
|
32
36
|
}
|
|
33
37
|
createFontConfig(locale, templateName, fontFamilyName) {
|
|
34
38
|
const template = getTemplate(templateName);
|
|
35
|
-
const fontFamily = fontFamilyName ? `${fontFamilyName}, ${SYSTEM_FONT_FALLBACKS[locale]}` : `${PREFERRED_FONTS[locale]}, ${SYSTEM_FONT_FALLBACKS[locale]}`;
|
|
39
|
+
const fontFamily = fontFamilyName ? `${quoteFontFamily(fontFamilyName)}, ${SYSTEM_FONT_FALLBACKS[locale]}` : `${PREFERRED_FONTS[locale]}, ${SYSTEM_FONT_FALLBACKS[locale]}`;
|
|
36
40
|
const textStyle = {
|
|
37
41
|
fontSize: template.textStyle?.fontSize || 40,
|
|
38
42
|
fontWeight: template.textStyle?.fontWeight || 400,
|
|
39
|
-
fontFamily: template.textStyle?.fontFamily || fontFamily,
|
|
43
|
+
fontFamily: fontFamilyName ? fontFamily : template.textStyle?.fontFamily || fontFamily,
|
|
40
44
|
fill: template.textStyle?.fill || "rgb(255, 255, 255)",
|
|
41
45
|
stroke: template.textStyle?.stroke,
|
|
42
46
|
strokeWidth: template.textStyle?.strokeWidth,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FontManager.js","sources":["../../../../src/stages/compose/font-system/FontManager.ts"],"sourcesContent":["import type { LocaleCode, FontConfig, SerializedFontConfig, TextStyle } from './types';\nimport { getTemplate } from './font-templates';\n\nconst SYSTEM_FONT_FALLBACKS: Record<LocaleCode, string> = {\n 'zh-CN': '-apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Microsoft YaHei\", sans-serif',\n 'en-US': '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif',\n 'ja-JP': '-apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Yu Gothic\", sans-serif',\n 'ko-KR': '-apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Malgun Gothic\", sans-serif',\n 'ar-SA': '-apple-system, BlinkMacSystemFont, \"Arabic UI Text\", \"Geeza Pro\", sans-serif',\n other: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif',\n};\n\nconst PREFERRED_FONTS: Record<LocaleCode, string> = {\n 'zh-CN': 'dtjinbu',\n 'en-US': 'dtjinbu',\n 'ja-JP': 'dtjinbu',\n 'ko-KR': 'dtjinbu',\n 'ar-SA': 'dtjinbu',\n other: 'dtjinbu',\n};\n\nexport class FontManager {\n private fontCache: Map<string, FontConfig> = new Map();\n\n getFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n ): SerializedFontConfig {\n const cacheKey = `${locale}-${templateName}-${fontFamilyName || 'default'}`;\n\n let config = this.fontCache.get(cacheKey);\n if (!config) {\n config = this.createFontConfig(locale, templateName, fontFamilyName);\n this.fontCache.set(cacheKey, config);\n }\n\n return {\n textStyle: { ...config.textStyle },\n containerStyle: config.containerStyle ? { ...config.containerStyle } : undefined,\n globalPosition: config.globalPosition ? { ...config.globalPosition } : undefined,\n };\n }\n\n private createFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n ): FontConfig {\n const template = getTemplate(templateName);\n\n const fontFamily = fontFamilyName\n ? `${fontFamilyName}, ${SYSTEM_FONT_FALLBACKS[locale]}`\n : `${PREFERRED_FONTS[locale]}, ${SYSTEM_FONT_FALLBACKS[locale]}`;\n\n const textStyle: TextStyle = {\n fontSize: template.textStyle?.fontSize || 40,\n fontWeight: template.textStyle?.fontWeight || 400,\n fontFamily: template.textStyle?.fontFamily || fontFamily,\n fill: template.textStyle?.fill || 'rgb(255, 255, 255)',\n stroke: template.textStyle?.stroke,\n strokeWidth: template.textStyle?.strokeWidth,\n letterSpacing: template.textStyle?.letterSpacing || 0,\n lineHeight: template.textStyle?.lineHeight || 1.2,\n paintOrder: template.textStyle?.paintOrder,\n strokeLinejoin: template.textStyle?.strokeLinejoin,\n strokeLinecap: template.textStyle?.strokeLinecap,\n };\n\n return {\n familyName: fontFamily,\n locales: [locale],\n textStyle,\n containerStyle: template.containerStyle,\n globalPosition: template.globalPosition,\n };\n }\n\n clearCache(): void {\n this.fontCache.clear();\n }\n}\n\nconst fontManagerInstance = new FontManager();\n\nexport function getFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n): SerializedFontConfig {\n return fontManagerInstance.getFontConfig(locale, templateName, fontFamilyName);\n}\n\nexport function clearFontCache(): void {\n fontManagerInstance.clearCache();\n}\n"],"names":[],"mappings":";AAGA,MAAM,wBAAoD;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,kBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEO,MAAM,YAAY;AAAA,EACf,gCAAyC,IAAA;AAAA,EAEjD,cACE,QACA,cACA,gBACsB;AACtB,UAAM,WAAW,GAAG,MAAM,IAAI,YAAY,IAAI,kBAAkB,SAAS;AAEzE,QAAI,SAAS,KAAK,UAAU,IAAI,QAAQ;AACxC,QAAI,CAAC,QAAQ;AACX,eAAS,KAAK,iBAAiB,QAAQ,cAAc,cAAc;AACnE,WAAK,UAAU,IAAI,UAAU,MAAM;AAAA,IACrC;AAEA,WAAO;AAAA,MACL,WAAW,EAAE,GAAG,OAAO,UAAA;AAAA,MACvB,gBAAgB,OAAO,iBAAiB,EAAE,GAAG,OAAO,mBAAmB;AAAA,MACvE,gBAAgB,OAAO,iBAAiB,EAAE,GAAG,OAAO,mBAAmB;AAAA,IAAA;AAAA,EAE3E;AAAA,EAEQ,iBACN,QACA,cACA,gBACY;AACZ,UAAM,WAAW,YAAY,YAAY;AAEzC,UAAM,aAAa,iBACf,GAAG,cAAc,KAAK,sBAAsB,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"FontManager.js","sources":["../../../../src/stages/compose/font-system/FontManager.ts"],"sourcesContent":["import type { LocaleCode, FontConfig, SerializedFontConfig, TextStyle } from './types';\nimport { getTemplate } from './font-templates';\n\nconst SYSTEM_FONT_FALLBACKS: Record<LocaleCode, string> = {\n 'zh-CN': '-apple-system, BlinkMacSystemFont, \"PingFang SC\", \"Microsoft YaHei\", sans-serif',\n 'en-US': '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif',\n 'ja-JP': '-apple-system, BlinkMacSystemFont, \"Hiragino Sans\", \"Yu Gothic\", sans-serif',\n 'ko-KR': '-apple-system, BlinkMacSystemFont, \"Apple SD Gothic Neo\", \"Malgun Gothic\", sans-serif',\n 'ar-SA': '-apple-system, BlinkMacSystemFont, \"Arabic UI Text\", \"Geeza Pro\", sans-serif',\n other: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif',\n};\n\nconst PREFERRED_FONTS: Record<LocaleCode, string> = {\n 'zh-CN': 'dtjinbu',\n 'en-US': 'dtjinbu',\n 'ja-JP': 'dtjinbu',\n 'ko-KR': 'dtjinbu',\n 'ar-SA': 'dtjinbu',\n other: 'dtjinbu',\n};\n\nfunction quoteFontFamily(fontFamily: string): string {\n const escaped = fontFamily.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n return `\"${escaped}\"`;\n}\n\nexport class FontManager {\n private fontCache: Map<string, FontConfig> = new Map();\n\n getFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n ): SerializedFontConfig {\n const cacheKey = `${locale}-${templateName}-${fontFamilyName || 'default'}`;\n\n let config = this.fontCache.get(cacheKey);\n if (!config) {\n config = this.createFontConfig(locale, templateName, fontFamilyName);\n this.fontCache.set(cacheKey, config);\n }\n\n return {\n textStyle: { ...config.textStyle },\n containerStyle: config.containerStyle ? { ...config.containerStyle } : undefined,\n globalPosition: config.globalPosition ? { ...config.globalPosition } : undefined,\n };\n }\n\n private createFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n ): FontConfig {\n const template = getTemplate(templateName);\n\n const fontFamily = fontFamilyName\n ? `${quoteFontFamily(fontFamilyName)}, ${SYSTEM_FONT_FALLBACKS[locale]}`\n : `${PREFERRED_FONTS[locale]}, ${SYSTEM_FONT_FALLBACKS[locale]}`;\n\n const textStyle: TextStyle = {\n fontSize: template.textStyle?.fontSize || 40,\n fontWeight: template.textStyle?.fontWeight || 400,\n fontFamily: fontFamilyName ? fontFamily : template.textStyle?.fontFamily || fontFamily,\n fill: template.textStyle?.fill || 'rgb(255, 255, 255)',\n stroke: template.textStyle?.stroke,\n strokeWidth: template.textStyle?.strokeWidth,\n letterSpacing: template.textStyle?.letterSpacing || 0,\n lineHeight: template.textStyle?.lineHeight || 1.2,\n paintOrder: template.textStyle?.paintOrder,\n strokeLinejoin: template.textStyle?.strokeLinejoin,\n strokeLinecap: template.textStyle?.strokeLinecap,\n };\n\n return {\n familyName: fontFamily,\n locales: [locale],\n textStyle,\n containerStyle: template.containerStyle,\n globalPosition: template.globalPosition,\n };\n }\n\n clearCache(): void {\n this.fontCache.clear();\n }\n}\n\nconst fontManagerInstance = new FontManager();\n\nexport function getFontConfig(\n locale: LocaleCode,\n templateName: string,\n fontFamilyName?: string\n): SerializedFontConfig {\n return fontManagerInstance.getFontConfig(locale, templateName, fontFamilyName);\n}\n\nexport function clearFontCache(): void {\n fontManagerInstance.clearCache();\n}\n"],"names":[],"mappings":";AAGA,MAAM,wBAAoD;AAAA,EACxD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,MAAM,kBAA8C;AAAA,EAClD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AACT;AAEA,SAAS,gBAAgB,YAA4B;AACnD,QAAM,UAAU,WAAW,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACrE,SAAO,IAAI,OAAO;AACpB;AAEO,MAAM,YAAY;AAAA,EACf,gCAAyC,IAAA;AAAA,EAEjD,cACE,QACA,cACA,gBACsB;AACtB,UAAM,WAAW,GAAG,MAAM,IAAI,YAAY,IAAI,kBAAkB,SAAS;AAEzE,QAAI,SAAS,KAAK,UAAU,IAAI,QAAQ;AACxC,QAAI,CAAC,QAAQ;AACX,eAAS,KAAK,iBAAiB,QAAQ,cAAc,cAAc;AACnE,WAAK,UAAU,IAAI,UAAU,MAAM;AAAA,IACrC;AAEA,WAAO;AAAA,MACL,WAAW,EAAE,GAAG,OAAO,UAAA;AAAA,MACvB,gBAAgB,OAAO,iBAAiB,EAAE,GAAG,OAAO,mBAAmB;AAAA,MACvE,gBAAgB,OAAO,iBAAiB,EAAE,GAAG,OAAO,mBAAmB;AAAA,IAAA;AAAA,EAE3E;AAAA,EAEQ,iBACN,QACA,cACA,gBACY;AACZ,UAAM,WAAW,YAAY,YAAY;AAEzC,UAAM,aAAa,iBACf,GAAG,gBAAgB,cAAc,CAAC,KAAK,sBAAsB,MAAM,CAAC,KACpE,GAAG,gBAAgB,MAAM,CAAC,KAAK,sBAAsB,MAAM,CAAC;AAEhE,UAAM,YAAuB;AAAA,MAC3B,UAAU,SAAS,WAAW,YAAY;AAAA,MAC1C,YAAY,SAAS,WAAW,cAAc;AAAA,MAC9C,YAAY,iBAAiB,aAAa,SAAS,WAAW,cAAc;AAAA,MAC5E,MAAM,SAAS,WAAW,QAAQ;AAAA,MAClC,QAAQ,SAAS,WAAW;AAAA,MAC5B,aAAa,SAAS,WAAW;AAAA,MACjC,eAAe,SAAS,WAAW,iBAAiB;AAAA,MACpD,YAAY,SAAS,WAAW,cAAc;AAAA,MAC9C,YAAY,SAAS,WAAW;AAAA,MAChC,gBAAgB,SAAS,WAAW;AAAA,MACpC,eAAe,SAAS,WAAW;AAAA,IAAA;AAGrC,WAAO;AAAA,MACL,YAAY;AAAA,MACZ,SAAS,CAAC,MAAM;AAAA,MAChB;AAAA,MACA,gBAAgB,SAAS;AAAA,MACzB,gBAAgB,SAAS;AAAA,IAAA;AAAA,EAE7B;AAAA,EAEA,aAAmB;AACjB,SAAK,UAAU,MAAA;AAAA,EACjB;AACF;AAEA,MAAM,sBAAsB,IAAI,YAAA;AAEzB,SAAS,cACd,QACA,cACA,gBACsB;AACtB,SAAO,oBAAoB,cAAc,QAAQ,cAAc,cAAc;AAC/E;"}
|
|
@@ -42,7 +42,7 @@ export interface SerializedFontConfig {
|
|
|
42
42
|
globalPosition?: GlobalPositionStyle;
|
|
43
43
|
}
|
|
44
44
|
export interface TextAnimationConfig {
|
|
45
|
-
type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';
|
|
45
|
+
type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp' | 'slideUp' | 'scale' | 'rotateScale' | 'blur' | 'flip3d' | 'typewriter' | 'letterSpread';
|
|
46
46
|
glowColor?: string;
|
|
47
47
|
glowIntensity?: number;
|
|
48
48
|
transitionFrames?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/font-system/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/font-system/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EACA,MAAM,GACN,MAAM,GACN,YAAY,GACZ,cAAc,GACd,iBAAiB,GACjB,mBAAmB,GACnB,SAAS,GACT,OAAO,GACP,aAAa,GACb,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,cAAc,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,SAAS,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -100,7 +100,7 @@ export interface SerializedTextLayerPayload extends LayerPayloadBase {
|
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
102
|
animation?: {
|
|
103
|
-
type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp';
|
|
103
|
+
type: 'none' | 'fade' | 'wordByWord' | 'characterKTV' | 'wordByWordFancy' | 'wordByWordSlideUp' | 'slideUp' | 'scale' | 'rotateScale' | 'blur' | 'flip3d' | 'typewriter' | 'letterSpread';
|
|
104
104
|
glowColor?: string;
|
|
105
105
|
glowIntensity?: number;
|
|
106
106
|
transitionFrames?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/instructions.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAGpE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,uBAAuB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,gBAAgB,CAAC;CACjC;AAED,MAAM,WAAW,2BAA4B,SAAQ,gBAAgB;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,YAAY,CAAC,EAAE,WAAW,CAAC;IAE3B,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACnC,SAAS,EAAE,KAAK,CAAC;YACf,IAAI,EAAE,MAAM,CAAC;YACb,SAAS,CAAC,EAAE;gBACV,CAAC,CAAC,EAAE,MAAM,CAAC;gBACX,CAAC,CAAC,EAAE,MAAM,CAAC;gBACX,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,MAAM,CAAC,EAAE,MAAM,CAAC;gBAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;gBAClB,OAAO,CAAC,EAAE,MAAM,CAAC;gBACjB,OAAO,CAAC,EAAE,MAAM,CAAC;aAClB,CAAC;YACF,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,MAAM,CAAC,EAAE,MAAM,CAAC;SACjB,CAAC,CAAC;QACH,kBAAkB,EAAE,MAAM,CAAC;QAC3B,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE;QACX,SAAS,EAAE;YACT,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;YAC5B,UAAU,EAAE,MAAM,CAAC;YACnB,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,WAAW,CAAC,EAAE,MAAM,CAAC;YACrB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;YAChC,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,cAAc,CAAC,EAAE,cAAc,CAAC;YAChC,aAAa,CAAC,EAAE,aAAa,CAAC;SAC/B,CAAC;QACF,cAAc,CAAC,EAAE;YACf,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,eAAe,CAAC,EAAE,MAAM,CAAC;YACzB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,YAAY,CAAC,EAAE,MAAM,CAAC;SACvB,CAAC;QACF,cAAc,CAAC,EAAE;YACf,QAAQ,CAAC,EAAE,UAAU,CAAC;YACtB,GAAG,CAAC,EAAE,MAAM,CAAC;YACb,MAAM,CAAC,EAAE,MAAM,CAAC;YAChB,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,UAAU,CAAC,EAAE,MAAM,CAAC;YACpB,cAAc,CAAC,EAAE,MAAM,CAAC;SACzB,CAAC;KACH,CAAC;IACF,SAAS,CAAC,EAAE;QACV,IAAI,EACA,MAAM,GACN,MAAM,GACN,YAAY,GACZ,cAAc,GACd,iBAAiB,GACjB,mBAAmB,GACnB,SAAS,GACT,OAAO,GACP,aAAa,GACb,MAAM,GACN,QAAQ,GACR,YAAY,GACZ,cAAc,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,kBAAkB,CAAC,EAAE;YACnB,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,MAAM,CAAC,EAAE,MAAM,CAAC;SACjB,CAAC;QACF,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF,WAAW,CAAC,EAAE,KAAK,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC,CAAC;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;CACzC;AAED,MAAM,WAAW,0BAA2B,SAAQ,gBAAgB;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,WAAW,CAAC;IAC3B,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAAC;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA6B,SAAQ,gBAAgB;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjF,MAAM,MAAM,mBAAmB,GAC3B,CAAC,uBAAuB,GAAG;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,2BAA2B,CAAC;CACtC,CAAC,GACF,CAAC,uBAAuB,GAAG;IACzB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,2BAA2B,CAAC;CACtC,CAAC,GACF,CAAC,uBAAuB,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,0BAA0B,CAAC;CACrC,CAAC,GACF,CAAC,uBAAuB,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,0BAA0B,CAAC;CACrC,CAAC,GACF,CAAC,uBAAuB,GAAG;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,4BAA4B,CAAC;CACvC,CAAC,CAAC;AAEP,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,MAAM,EAAE,0BAA0B,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,MAAM,EAAE,qBAAqB,CAAC;CAC/B"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TextLayer } from './types';
|
|
2
|
+
|
|
3
|
+
export declare function attachmentEntranceRelativeTimeUs(clipRelativeTimeUs: number, activeRanges: Array<{
|
|
4
|
+
startUs: number;
|
|
5
|
+
endUs: number;
|
|
6
|
+
}> | undefined): number | undefined;
|
|
7
|
+
export declare function textAnimationFrameForTextLayer(layer: TextLayer, fallbackFrame: number, fps: number): number;
|
|
8
|
+
//# sourceMappingURL=text-layer-animation-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-layer-animation-time.d.ts","sourceRoot":"","sources":["../../../src/stages/compose/text-layer-animation-time.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,wBAAgB,gCAAgC,CAC9C,kBAAkB,EAAE,MAAM,EAC1B,YAAY,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,GAClE,MAAM,GAAG,SAAS,CAQpB;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,SAAS,EAChB,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,MAAM,GACV,MAAM,CAOR"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function attachmentEntranceRelativeTimeUs(clipRelativeTimeUs, activeRanges) {
|
|
2
|
+
const containing = activeRanges?.find(
|
|
3
|
+
(r) => clipRelativeTimeUs >= r.startUs && clipRelativeTimeUs < r.endUs
|
|
4
|
+
);
|
|
5
|
+
if (!containing) {
|
|
6
|
+
return void 0;
|
|
7
|
+
}
|
|
8
|
+
return Math.max(0, clipRelativeTimeUs - containing.startUs);
|
|
9
|
+
}
|
|
10
|
+
function textAnimationFrameForTextLayer(layer, fallbackFrame, fps) {
|
|
11
|
+
const et = layer.entranceRelativeTimeUs;
|
|
12
|
+
if (et != null && Number.isFinite(et)) {
|
|
13
|
+
const frameDurationUs = 1e6 / fps;
|
|
14
|
+
return Math.floor(et / frameDurationUs);
|
|
15
|
+
}
|
|
16
|
+
return fallbackFrame;
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
attachmentEntranceRelativeTimeUs,
|
|
20
|
+
textAnimationFrameForTextLayer
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=text-layer-animation-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-layer-animation-time.js","sources":["../../../src/stages/compose/text-layer-animation-time.ts"],"sourcesContent":["import type { TextLayer } from './types';\n\nexport function attachmentEntranceRelativeTimeUs(\n clipRelativeTimeUs: number,\n activeRanges: Array<{ startUs: number; endUs: number }> | undefined\n): number | undefined {\n const containing = activeRanges?.find(\n (r) => clipRelativeTimeUs >= r.startUs && clipRelativeTimeUs < r.endUs\n );\n if (!containing) {\n return undefined;\n }\n return Math.max(0, clipRelativeTimeUs - containing.startUs);\n}\n\nexport function textAnimationFrameForTextLayer(\n layer: TextLayer,\n fallbackFrame: number,\n fps: number\n): number {\n const et = layer.entranceRelativeTimeUs;\n if (et != null && Number.isFinite(et)) {\n const frameDurationUs = 1_000_000 / fps;\n return Math.floor(et / frameDurationUs);\n }\n return fallbackFrame;\n}\n"],"names":[],"mappings":"AAEO,SAAS,iCACd,oBACA,cACoB;AACpB,QAAM,aAAa,cAAc;AAAA,IAC/B,CAAC,MAAM,sBAAsB,EAAE,WAAW,qBAAqB,EAAE;AAAA,EAAA;AAEnE,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,GAAG,qBAAqB,WAAW,OAAO;AAC5D;AAEO,SAAS,+BACd,OACA,eACA,KACQ;AACR,QAAM,KAAK,MAAM;AACjB,MAAI,MAAM,QAAQ,OAAO,SAAS,EAAE,GAAG;AACrC,UAAM,kBAAkB,MAAY;AACpC,WAAO,KAAK,MAAM,KAAK,eAAe;AAAA,EACxC;AACA,SAAO;AACT;"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { formEvenLinesWithWords, wrapText } from "../text-utils/text-wrapper.js";
|
|
2
2
|
import { getLetterCaseText } from "../text-utils/text-metrics.js";
|
|
3
|
-
import { interpolate, springEasing } from "./animation-utils.js";
|
|
4
3
|
import { needsSpaceBetweenWords } from "../text-utils/locale-detector.js";
|
|
5
4
|
function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
|
|
6
5
|
if (!globalPosition) {
|
|
@@ -68,26 +67,7 @@ function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame)
|
|
|
68
67
|
}
|
|
69
68
|
ctx.restore();
|
|
70
69
|
}
|
|
71
|
-
function renderTextWithEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame) {
|
|
72
|
-
const fontConfig = layer.fontConfig?.textStyle;
|
|
73
|
-
if (!fontConfig) return;
|
|
74
|
-
const entrance = springEasing(relativeFrame, {
|
|
75
|
-
damping: 200,
|
|
76
|
-
mass: 1,
|
|
77
|
-
stiffness: 300
|
|
78
|
-
});
|
|
79
|
-
const opacity = interpolate(entrance, [0, 1], [0, 1]);
|
|
80
|
-
const scale = interpolate(entrance, [0, 1], [0.9, 1]);
|
|
81
|
-
ctx.save();
|
|
82
|
-
ctx.globalAlpha = opacity;
|
|
83
|
-
ctx.translate(canvasWidth / 2, canvasHeight / 2);
|
|
84
|
-
ctx.scale(scale, scale);
|
|
85
|
-
ctx.translate(-canvasWidth / 2, -canvasHeight / 2);
|
|
86
|
-
renderBasicText(ctx, layer, canvasWidth, canvasHeight);
|
|
87
|
-
ctx.restore();
|
|
88
|
-
}
|
|
89
70
|
export {
|
|
90
|
-
renderBasicText
|
|
91
|
-
renderTextWithEntrance
|
|
71
|
+
renderBasicText
|
|
92
72
|
};
|
|
93
73
|
//# sourceMappingURL=basic-text-renderer.js.map
|