@@ -23,6 +23,8 @@ import * as pixi from "pixi.js";
2323
2424import { Entity } from "../../../core/shared/entity" ;
2525
26+ import { COLOR_FILTERS , ColorFilter } from "./color-filter" ;
27+
2628/**
2729 * Tracks a merge field binding for a specific property path.
2830 * Used to restore placeholders on export for properties that haven't changed.
@@ -52,6 +54,21 @@ export enum PlayerType {
5254 RichCaption = "rich-caption"
5355}
5456
57+ // The render filters clips it draws as images and leaves those it plays as video untouched.
58+ const FILTERED_PLAYERS = new Set ( [
59+ PlayerType . Image ,
60+ PlayerType . Text ,
61+ PlayerType . RichText ,
62+ PlayerType . Shape ,
63+ PlayerType . Caption ,
64+ PlayerType . Html ,
65+ PlayerType . Svg ,
66+ PlayerType . TextToImage
67+ ] ) ;
68+
69+ // pixi's Gaussian at this strength spreads as far as the render's radius-10 box blur (σ ≈ 6.06 px).
70+ const CLIP_BLUR_STRENGTH = 4.69 ;
71+
5572/**
5673 * Base class for all visual content players in the canvas.
5774 *
@@ -101,6 +118,9 @@ export abstract class Player extends Entity {
101118 private wipeFilter : WipeFilter | null = null ;
102119 private zoomBlurBuilder ?: KeyframeBuilder ;
103120 private zoomBlurFilter : pixi . BlurFilter | null = null ;
121+ private zoomBlurRadius = 0 ;
122+ private clipFilterName : ResolvedClip [ "filter" ] ;
123+ private clipFilter : pixi . Filter | null = null ;
104124 private rotationOrigin ?: { x : KeyframeBuilder ; y : KeyframeBuilder } ;
105125
106126 private wipeMask : pixi . Graphics | null ;
@@ -126,6 +146,7 @@ export abstract class Player extends Entity {
126146 this . contentContainer = new pixi . Container ( ) ;
127147 this . lumaWrapper . addChild ( this . contentContainer ) ;
128148 this . getContainer ( ) . addChild ( this . lumaWrapper ) ;
149+ this . getContainer ( ) . onRender = ( ) => this . sizeBlurs ( ) ;
129150 }
130151
131152 public reconfigureAfterRestore ( ) : void {
@@ -265,6 +286,8 @@ export abstract class Player extends Entity {
265286 this . applyFixedDimensions ( ) ;
266287 }
267288
289+ this . updateClipFilter ( ) ;
290+
268291 // Update wipe/reveal mask animation
269292 this . updateWipeMask ( ) ;
270293 this . updateWipeFilter ( ) ;
@@ -301,14 +324,43 @@ export abstract class Player extends Entity {
301324 this . wipeMask . fill ( 0xffffff ) ;
302325 }
303326
327+ private updateClipFilter ( ) : void {
328+ const { asset } = this . clipConfiguration ;
329+ const rendersAsVideo = asset . type === "rich-text" && Boolean ( asset . animation ?. preset ) ;
330+ const name = FILTERED_PLAYERS . has ( this . playerType ) && ! rendersAsVideo ? this . clipConfiguration . filter : undefined ;
331+
332+ if ( name !== this . clipFilterName ) {
333+ this . clipFilter ?. destroy ( ) ;
334+ this . clipFilter = this . createClipFilter ( name ) ;
335+ this . clipFilterName = name ;
336+ this . syncContentFilters ( ) ;
337+ }
338+ }
339+
340+ private createClipFilter ( name : ResolvedClip [ "filter" ] ) : pixi . Filter | null {
341+ if ( name === "blur" ) {
342+ const blur = new pixi . BlurFilter ( ) ;
343+ // The render's box blur clamps at the clip's edges, so they stay sharp.
344+ blur . repeatEdgePixels = true ;
345+ return blur ;
346+ }
347+ const color = name && COLOR_FILTERS [ name ] ;
348+ return color ? new ColorFilter ( color ) : null ;
349+ }
350+
351+ private syncContentFilters ( ) : void {
352+ // The render filters a clip before its transitions.
353+ this . contentContainer . filters = [ this . clipFilter , this . wipeFilter ] . filter ( ( filter ) : filter is pixi . Filter => filter !== null ) ;
354+ }
355+
304356 private updateWipeFilter ( ) : void {
305357 if ( ! this . wipeBrightnessBuilder ) {
306358 this . removeWipeFilter ( ) ;
307359 return ;
308360 }
309361 if ( ! this . wipeFilter ) {
310362 this . wipeFilter = new WipeFilter ( ) ;
311- this . contentContainer . filters = [ this . wipeFilter ] ;
363+ this . syncContentFilters ( ) ;
312364 }
313365 const time = this . getPlaybackTime ( ) ;
314366 this . wipeFilter . brightness = this . wipeBrightnessBuilder . getValue ( time ) ;
@@ -317,9 +369,9 @@ export abstract class Player extends Entity {
317369
318370 private removeWipeFilter ( ) : void {
319371 if ( ! this . wipeFilter ) return ;
320- this . contentContainer . filters = [ ] ;
321372 this . wipeFilter . destroy ( ) ;
322373 this . wipeFilter = null ;
374+ this . syncContentFilters ( ) ;
323375 }
324376
325377 private updateZoomBlur ( ) : void {
@@ -334,14 +386,19 @@ export abstract class Player extends Entity {
334386 this . zoomBlurFilter = new pixi . BlurFilter ( ) ;
335387 this . getContainer ( ) . filters = [ this . zoomBlurFilter ] ;
336388 }
337- // The render blurs by a whole-pixel radius before applying the clip's scale, so on screen the blur grows with scale
338- // and the canvas zoom. pixi's Gaussian approximates the render's three box-blur passes.
339- const time = this . getPlaybackTime ( ) ;
340- const radius = Math . trunc ( this . zoomBlurBuilder . getValue ( time ) ) ;
341- const canvas = this . getContainer ( ) . parent ?. worldTransform ;
342- const canvasZoom = canvas ? Math . hypot ( canvas . a , canvas . b ) : 1 ;
343- this . zoomBlurFilter . enabled = radius > 0 ;
344- this . zoomBlurFilter . strength = radius * ( this . scaleKeyframeBuilder ?. getValue ( time ) ?? 1 ) * canvasZoom ;
389+ this . zoomBlurRadius = Math . trunc ( this . zoomBlurBuilder . getValue ( this . getPlaybackTime ( ) ) ) ;
390+ this . zoomBlurFilter . enabled = this . zoomBlurRadius > 0 ;
391+ }
392+
393+ // The render blurs by whole pixels before applying the clip's scale, so a blur grows with scale and the zoom it is
394+ // drawn at. Sizing it as each frame draws follows that zoom: the editor's on screen, 1:1 in a capture.
395+ private sizeBlurs ( ) : void {
396+ if ( ! this . zoomBlurFilter && ! ( this . clipFilter instanceof pixi . BlurFilter ) ) return ;
397+ const canvas = this . getContainer ( ) . parent ?. getGlobalTransform ( ) ;
398+ const scale = ( this . scaleKeyframeBuilder ?. getValue ( this . getPlaybackTime ( ) ) ?? 1 ) * ( canvas ? Math . hypot ( canvas . a , canvas . b ) : 1 ) ;
399+ // pixi's Gaussian approximates the render's three box-blur passes.
400+ if ( this . zoomBlurFilter ) this . zoomBlurFilter . strength = this . zoomBlurRadius * scale ;
401+ if ( this . clipFilter instanceof pixi . BlurFilter ) this . clipFilter . strength = CLIP_BLUR_STRENGTH * scale ;
345402 }
346403
347404 public override dispose ( ) : void {
@@ -351,6 +408,9 @@ export abstract class Player extends Entity {
351408 this . wipeFilter = null ;
352409 this . zoomBlurFilter ?. destroy ( ) ;
353410 this . zoomBlurFilter = null ;
411+ this . clipFilter ?. destroy ( ) ;
412+ this . clipFilter = null ;
413+ this . clipFilterName = undefined ;
354414
355415 this . contentContainer ?. destroy ( ) ;
356416 this . lumaWrapper ?. destroy ( ) ;
0 commit comments