@@ -275,14 +275,196 @@ public static Image FillMaskWithTiledImage(
275275 Image < Rgba32 > result = targetImage . Clone ( ) ;
276276 var random = new Random ( ) ;
277277
278- // Resize tile image based on target image width ratio (1/40 for 32px tiles on 1280px width)
279- const double tileRatio = 1.0 / 40.0 ;
280- var desiredTileSize = ( int ) ( targetImage . Width * tileRatio ) ;
278+ // Calculate average color of tile image for background fill
279+ Rgba32 averageColor = CalculateAverageColor ( tileImage ) ;
280+
281+ // Apply brightness reduction to the average color for Layer 1
282+ const float baseLayerBrightness = 0.8f ;
283+ var darkenedAverageColor = new Rgba32 (
284+ ( byte ) ( averageColor . R * baseLayerBrightness ) ,
285+ ( byte ) ( averageColor . G * baseLayerBrightness ) ,
286+ ( byte ) ( averageColor . B * baseLayerBrightness ) ,
287+ averageColor . A ) ;
288+
289+ // Layer 1: Fill entire masked area with darkened average color using pixel-level processing
290+ for ( var y = 0 ; y < Math . Min ( result . Height , maskImage . Height ) ; y ++ )
291+ {
292+ for ( var x = 0 ; x < Math . Min ( result . Width , maskImage . Width ) ; x ++ )
293+ {
294+ Rgba32 maskPixel = maskImage [ x , y ] ;
295+ if ( maskPixel is { R : > 200 , G : > 200 , B : > 200 } )
296+ {
297+ result [ x , y ] = darkenedAverageColor ;
298+ }
299+ }
300+ }
301+
302+ // Layer 2: Texture-like coverage with reduced brightness (depth effect)
303+ const double textureLayerTileRatio = 1.0 / 34.0 ;
304+ const float textureLayerBrightness = 0.9f ;
305+ const float textureLayerSpacingMultiplier = 0.3f ;
306+
307+ ApplyTextureLayer (
308+ result ,
309+ maskImage ,
310+ tileImage ,
311+ random ,
312+ textureLayerTileRatio ,
313+ textureLayerBrightness ,
314+ textureLayerSpacingMultiplier ) ;
315+
316+ // Layer 3: Higher tile ratio with original brightness and increased spacing
317+ const double tileLayerTileRatio = 1.0 / 30.0 ;
318+ const float tileLayerBrightness = 1.0f ;
319+ const float tileLayerSpacingMultiplier = 0.65f ;
320+
321+ ApplyTileLayer (
322+ result ,
323+ maskImage ,
324+ tileImage ,
325+ random ,
326+ tileLayerTileRatio ,
327+ tileLayerBrightness ,
328+ tileLayerSpacingMultiplier ) ;
329+
330+ return result ;
331+ }
332+
333+ private static void ApplyTextureLayer (
334+ Image < Rgba32 > result ,
335+ Image < Rgba32 > maskImage ,
336+ Image < Rgba32 > tileImage ,
337+ Random random ,
338+ double tileRatio ,
339+ float brightness ,
340+ float spacingMultiplier )
341+ {
342+ var desiredTileSize = ( int ) ( result . Width * tileRatio ) ;
343+
344+ // Resize the tile image to maintain square aspect ratio
345+ using Image < Rgba32 > resizedTileImage = tileImage . Clone ( ctx =>
346+ ctx . Resize ( desiredTileSize , desiredTileSize , KnownResamplers . Hermite ) ) ;
347+
348+ int tileWidth = resizedTileImage . Width ;
349+ int tileHeight = resizedTileImage . Height ;
350+ int maxJitterX = tileWidth / 4 ; // Reduced jitter for more consistent coverage
351+ int maxJitterY = tileHeight / 4 ;
352+
353+ // Create a grid that ensures complete coverage with some overlap
354+ var gridSpacingX = ( int ) ( tileWidth * spacingMultiplier ) ;
355+ var gridSpacingY = ( int ) ( tileHeight * spacingMultiplier ) ;
356+
357+ // Start with slight negative offset to ensure edge coverage
358+ int startX = - tileWidth / 2 ;
359+ int startY = - tileHeight / 2 ;
360+
361+ for ( int gridY = startY ; gridY < maskImage . Height + tileHeight ; gridY += gridSpacingY )
362+ {
363+ for ( int gridX = startX ; gridX < maskImage . Width + tileWidth ; gridX += gridSpacingX )
364+ {
365+ // Add small random jitter to avoid perfect grid pattern
366+ int jitterX = random . Next ( - maxJitterX , maxJitterX ) ;
367+ int jitterY = random . Next ( - maxJitterY , maxJitterY ) ;
368+
369+ int tileX = gridX + jitterX ;
370+ int tileY = gridY + jitterY ;
371+
372+ // Check if this tile position overlaps with the mask
373+ var overlapsMask = false ;
374+ int checkRadius = Math . Max ( tileWidth , tileHeight ) / 2 ;
375+
376+ for ( int checkY = Math . Max ( val1 : 0 , tileY ) ;
377+ checkY < Math . Min ( maskImage . Height , tileY + tileHeight ) && ! overlapsMask ;
378+ checkY += checkRadius / 2 )
379+ {
380+ for ( int checkX = Math . Max ( val1 : 0 , tileX ) ;
381+ checkX < Math . Min ( maskImage . Width , tileX + tileWidth ) && ! overlapsMask ;
382+ checkX += checkRadius / 2 )
383+ {
384+ if ( checkX < maskImage . Width && checkY < maskImage . Height )
385+ {
386+ Rgba32 pixel = maskImage [ checkX , checkY ] ;
387+ if ( pixel is { R : > 200 , G : > 200 , B : > 200 } )
388+ {
389+ overlapsMask = true ;
390+ }
391+ }
392+ }
393+ }
394+
395+ if ( overlapsMask )
396+ {
397+ // Draw the tile image cropped to the mask boundaries using direct pixel access
398+ for ( var y = 0 ; y < tileHeight ; y ++ )
399+ {
400+ int resultY = tileY + y ;
401+ if ( resultY < 0 || resultY >= result . Height ) continue ;
402+
403+ for ( var x = 0 ; x < tileWidth ; x ++ )
404+ {
405+ int resultX = tileX + x ;
406+ if ( resultX < 0 || resultX >= result . Width ) continue ;
407+
408+ // Check if this position is within the mask
409+ if ( resultX < maskImage . Width && resultY < maskImage . Height )
410+ {
411+ Rgba32 maskPixel = maskImage [ resultX , resultY ] ;
412+ if ( maskPixel is { R : > 200 , G : > 200 , B : > 200 } )
413+ {
414+ // Draw tile pixels within the mask, including semi-transparent ones for smooth edges
415+ Rgba32 tilePixel = resizedTileImage [ x , y ] ;
416+ if ( tilePixel . A > 0 ) // Include all non-fully-transparent pixels
417+ {
418+ // Apply brightness reduction per-pixel to preserve edge anti-aliasing
419+ var adjustedPixel = new Rgba32 (
420+ ( byte ) ( tilePixel . R * brightness ) ,
421+ ( byte ) ( tilePixel . G * brightness ) ,
422+ ( byte ) ( tilePixel . B * brightness ) ,
423+ tilePixel . A ) ; // Keep original alpha
424+
425+ // Blend with existing pixel using alpha blending
426+ Rgba32 existingPixel = result [ resultX , resultY ] ;
427+ float alpha = adjustedPixel . A / 255f ;
428+ float invAlpha = 1f - alpha ;
429+
430+ var blendedPixel = new Rgba32 (
431+ ( byte ) ( ( adjustedPixel . R * alpha ) + ( existingPixel . R * invAlpha ) ) ,
432+ ( byte ) ( ( adjustedPixel . G * alpha ) + ( existingPixel . G * invAlpha ) ) ,
433+ ( byte ) ( ( adjustedPixel . B * alpha ) + ( existingPixel . B * invAlpha ) ) ,
434+ Math . Max ( adjustedPixel . A , existingPixel . A ) ) ;
435+
436+ result [ resultX , resultY ] = blendedPixel ;
437+ }
438+ }
439+ }
440+ }
441+ }
442+ }
443+ }
444+ }
445+ }
446+
447+ private static void ApplyTileLayer (
448+ Image < Rgba32 > result ,
449+ Image < Rgba32 > maskImage ,
450+ Image < Rgba32 > tileImage ,
451+ Random random ,
452+ double tileRatio ,
453+ float brightness ,
454+ float spacingMultiplier )
455+ {
456+ var desiredTileSize = ( int ) ( result . Width * tileRatio ) ;
281457
282458 // Resize the tile image to maintain square aspect ratio
283- Image < Rgba32 > resizedTileImage = tileImage . Clone ( ctx =>
459+ using Image < Rgba32 > resizedTileImage = tileImage . Clone ( ctx =>
284460 ctx . Resize ( desiredTileSize , desiredTileSize , KnownResamplers . Hermite ) ) ;
285461
462+ // Adjust brightness if needed
463+ if ( Math . Abs ( brightness - 1.0f ) > 0.001f )
464+ {
465+ resizedTileImage . Mutate ( ctx => ctx . Brightness ( brightness ) ) ;
466+ }
467+
286468 // Calculate tile placement parameters
287469 int tileWidth = resizedTileImage . Width ;
288470 int tileHeight = resizedTileImage . Height ;
@@ -293,16 +475,19 @@ public static Image FillMaskWithTiledImage(
293475 var maskRegions = new List < Point > ( ) ;
294476
295477 // Scan for white pixels in the mask (sampling at intervals)
296- for ( var y = 0 ; y < maskImage . Height ; y += tileHeight / 2 )
478+ var spacingY = ( int ) ( tileHeight * spacingMultiplier ) ;
479+ var spacingX = ( int ) ( tileWidth * spacingMultiplier ) ;
480+
481+ for ( var y = 0 ; y < maskImage . Height ; y += spacingY )
297482 {
298- for ( var x = 0 ; x < maskImage . Width ; x += tileWidth / 2 )
483+ for ( var x = 0 ; x < maskImage . Width ; x += spacingX )
299484 {
300485 if ( x < maskImage . Width && y < maskImage . Height )
301486 {
302487 Rgba32 pixel = maskImage [ x , y ] ;
303488
304489 // Check if pixel is white (high RGB values)
305- if ( pixel . R > 200 && pixel . G > 200 && pixel . B > 200 )
490+ if ( pixel is { R : > 200 , G : > 200 , B : > 200 } )
306491 {
307492 maskRegions . Add ( new Point ( x , y ) ) ;
308493 }
@@ -329,19 +514,48 @@ public static Image FillMaskWithTiledImage(
329514 centerY >= 0 && centerY < maskImage . Height )
330515 {
331516 Rgba32 centerPixel = maskImage [ centerX , centerY ] ;
332- if ( centerPixel . R > 200 && centerPixel . G > 200 && centerPixel . B > 200 )
517+ if ( centerPixel is { R : > 200 , G : > 200 , B : > 200 } )
333518 {
334- // Ensure tile placement is within target image bounds
335- tileX = Math . Max ( val1 : 0 , Math . Min ( tileX , targetImage . Width - tileWidth ) ) ;
336- tileY = Math . Max ( val1 : 0 , Math . Min ( tileY , targetImage . Height - tileHeight ) ) ;
337-
338- // Draw the tile image at the calculated position
519+ // Allow tiles to extend beyond image bounds for natural edge effect
520+ // Draw the tile image at the calculated position (can go partially offscreen)
339521 result . Mutate ( ctx => { ctx . DrawImage ( resizedTileImage , new Point ( tileX , tileY ) , opacity : 1f ) ; } ) ;
340522 }
341523 }
342524 }
525+ }
343526
344- resizedTileImage . Dispose ( ) ;
345- return result ;
527+ private static Rgba32 CalculateAverageColor ( Image < Rgba32 > image )
528+ {
529+ long totalR = 0 , totalG = 0 , totalB = 0 , totalA = 0 ;
530+ var pixelCount = 0 ;
531+
532+ image . ProcessPixelRows ( accessor =>
533+ {
534+ for ( var y = 0 ; y < accessor . Height ; y ++ )
535+ {
536+ Span < Rgba32 > pixelRow = accessor . GetRowSpan ( y ) ;
537+ for ( var x = 0 ; x < pixelRow . Length ; x ++ )
538+ {
539+ Rgba32 pixel = pixelRow [ x ] ;
540+ if ( pixel . A > 0 ) // Only count non-transparent pixels
541+ {
542+ totalR += pixel . R ;
543+ totalG += pixel . G ;
544+ totalB += pixel . B ;
545+ totalA += pixel . A ;
546+ pixelCount ++ ;
547+ }
548+ }
549+ }
550+ } ) ;
551+
552+ if ( pixelCount == 0 )
553+ return new Rgba32 ( r : 0 , g : 0 , b : 0 , a : 0 ) ;
554+
555+ return new Rgba32 (
556+ ( byte ) ( totalR / pixelCount ) ,
557+ ( byte ) ( totalG / pixelCount ) ,
558+ ( byte ) ( totalB / pixelCount ) ,
559+ ( byte ) ( totalA / pixelCount ) ) ;
346560 }
347561}
0 commit comments