@@ -694,6 +694,9 @@ local matrixSize = 4`
694694 {1, 0, 1, 0}
695695}
696696local matrixSize = 4`
697+ case "floyd_steinberg" :
698+ // Floyd-Steinberg uses error diffusion instead of matrix patterns
699+ return generateFloydSteinbergLua (escapedLayerName , frameNumber , x , y , width , height , c1 , c2 , density )
697700 default :
698701 return fmt .Sprintf (`error("Unknown dithering pattern: %s")` , pattern )
699702 }
@@ -809,3 +812,245 @@ print("Dithering applied successfully")`,
809812 height , width ,
810813 x , y )
811814}
815+
816+ // generateFloydSteinbergLua generates Lua script for Floyd-Steinberg error diffusion dithering.
817+ //
818+ // Floyd-Steinberg is an error diffusion algorithm that produces high-quality dithering
819+ // by propagating quantization error to neighboring pixels. The error distribution pattern is:
820+ //
821+ // X 7/16
822+ // 3/16 5/16 1/16
823+ //
824+ // where X is the current pixel being processed.
825+ //
826+ // The implementation creates a horizontal gradient from color1 to color2 and applies error
827+ // diffusion to produce smooth transitions. The density parameter is currently unused as the
828+ // gradient quality is controlled by the error diffusion algorithm itself.
829+ func generateFloydSteinbergLua (layerName string , frameNumber int , x , y , width , height int , c1 , c2 Color , density float64 ) string {
830+ return fmt .Sprintf (`local spr = app.activeSprite
831+ if not spr then
832+ error("No active sprite")
833+ end
834+
835+ -- Find layer
836+ local layer = nil
837+ for _, l in ipairs(spr.layers) do
838+ if l.name == "%s" then
839+ layer = l
840+ break
841+ end
842+ end
843+
844+ if not layer then
845+ error("Layer not found: %s")
846+ end
847+
848+ -- Get frame
849+ local frame = spr.frames[%d]
850+ if not frame then
851+ error("Frame not found: %d")
852+ end
853+
854+ -- Get or create cel
855+ local cel = layer:cel(frame)
856+ if not cel then
857+ cel = spr:newCel(layer, frame)
858+ end
859+
860+ -- Create or get image
861+ local img = cel.image
862+ if not img then
863+ img = Image(spr.width, spr.height, spr.colorMode)
864+ cel.image = img
865+ end
866+
867+ -- Helper: Find nearest palette index for given RGBA color
868+ local function findNearestPaletteIndex(r, g, b, a)
869+ local palette = spr.palettes[1]
870+ if not palette or #palette == 0 then
871+ return 0
872+ end
873+
874+ local minDist = math.huge
875+ local nearestIndex = 0
876+
877+ for i = 0, #palette - 1 do
878+ local palColor = palette:getColor(i)
879+ local dr = r - palColor.red
880+ local dg = g - palColor.green
881+ local db = b - palColor.blue
882+ local da = a - palColor.alpha
883+ local dist = dr*dr + dg*dg + db*db + da*da
884+
885+ if dist < minDist then
886+ minDist = dist
887+ nearestIndex = i
888+ end
889+ end
890+
891+ return nearestIndex
892+ end
893+
894+ -- Define colors based on color mode
895+ local color1, color2
896+ local color1_r, color1_g, color1_b, color1_a = %d, %d, %d, %d
897+ local color2_r, color2_g, color2_b, color2_a = %d, %d, %d, %d
898+
899+ if spr.colorMode == ColorMode.INDEXED then
900+ -- In indexed mode, img:drawPixel expects palette indices
901+ color1 = findNearestPaletteIndex(color1_r, color1_g, color1_b, color1_a)
902+ color2 = findNearestPaletteIndex(color2_r, color2_g, color2_b, color2_a)
903+ else
904+ -- In RGB/Grayscale mode, use pixel color values
905+ color1 = app.pixelColor.rgba(color1_r, color1_g, color1_b, color1_a)
906+ color2 = app.pixelColor.rgba(color2_r, color2_g, color2_b, color2_a)
907+ end
908+
909+ -- Floyd-Steinberg error diffusion
910+ app.transaction(function()
911+ -- Create error buffer (width+2 to handle edges, 2 rows for current and next)
912+ local errors = {}
913+ for row = 0, 1 do
914+ errors[row] = {}
915+ for col = 0, %d + 1 do
916+ errors[row][col] = {r=0, g=0, b=0}
917+ end
918+ end
919+
920+ for py = 0, %d - 1 do
921+ -- Swap error buffers for next row
922+ if py > 0 then
923+ errors[0], errors[1] = errors[1], errors[0]
924+ -- Clear the new "next row" buffer
925+ for col = 0, %d + 1 do
926+ errors[1][col] = {r=0, g=0, b=0}
927+ end
928+ end
929+
930+ for px = 0, %d - 1 do
931+ -- Calculate gradient position (0.0 to 1.0 across width)
932+ local gradient_pos
933+ if %d == 1 then
934+ gradient_pos = 0
935+ else
936+ gradient_pos = px / (%d - 1)
937+ end
938+
939+ -- Calculate ideal gradient color at this position
940+ local ideal_r = color1_r + (color2_r - color1_r) * gradient_pos
941+ local ideal_g = color1_g + (color2_g - color1_g) * gradient_pos
942+ local ideal_b = color1_b + (color2_b - color1_b) * gradient_pos
943+
944+ -- Add accumulated error
945+ local err = errors[0][px + 1]
946+ local new_r = math.max(0, math.min(255, ideal_r + err.r))
947+ local new_g = math.max(0, math.min(255, ideal_g + err.g))
948+ local new_b = math.max(0, math.min(255, ideal_b + err.b))
949+
950+ -- Calculate color with accumulated error
951+ local targetColor
952+ if spr.colorMode == ColorMode.INDEXED then
953+ -- Choose nearest color by Euclidean distance
954+ local dist1 = (new_r - color1_r)^2 + (new_g - color1_g)^2 + (new_b - color1_b)^2
955+ local dist2 = (new_r - color2_r)^2 + (new_g - color2_g)^2 + (new_b - color2_b)^2
956+
957+ targetColor = (dist1 < dist2) and color1 or color2
958+
959+ -- Calculate error in RGB space
960+ local actual_r, actual_g, actual_b
961+ if targetColor == color1 then
962+ actual_r, actual_g, actual_b = color1_r, color1_g, color1_b
963+ else
964+ actual_r, actual_g, actual_b = color2_r, color2_g, color2_b
965+ end
966+
967+ local err_r = new_r - actual_r
968+ local err_g = new_g - actual_g
969+ local err_b = new_b - actual_b
970+
971+ -- Distribute error to neighbors (Floyd-Steinberg weights)
972+ if px < %d - 1 then
973+ errors[0][px + 2].r = errors[0][px + 2].r + err_r * 7/16
974+ errors[0][px + 2].g = errors[0][px + 2].g + err_g * 7/16
975+ errors[0][px + 2].b = errors[0][px + 2].b + err_b * 7/16
976+ end
977+ if py < %d - 1 then
978+ if px > 0 then
979+ errors[1][px].r = errors[1][px].r + err_r * 3/16
980+ errors[1][px].g = errors[1][px].g + err_g * 3/16
981+ errors[1][px].b = errors[1][px].b + err_b * 3/16
982+ end
983+ errors[1][px + 1].r = errors[1][px + 1].r + err_r * 5/16
984+ errors[1][px + 1].g = errors[1][px + 1].g + err_g * 5/16
985+ errors[1][px + 1].b = errors[1][px + 1].b + err_b * 5/16
986+ if px < %d - 1 then
987+ errors[1][px + 2].r = errors[1][px + 2].r + err_r * 1/16
988+ errors[1][px + 2].g = errors[1][px + 2].g + err_g * 1/16
989+ errors[1][px + 2].b = errors[1][px + 2].b + err_b * 1/16
990+ end
991+ end
992+ else
993+ -- RGB mode - choose nearest color by Euclidean distance
994+ local dist1 = (new_r - color1_r)^2 + (new_g - color1_g)^2 + (new_b - color1_b)^2
995+ local dist2 = (new_r - color2_r)^2 + (new_g - color2_g)^2 + (new_b - color2_b)^2
996+
997+ targetColor = (dist1 < dist2) and color1 or color2
998+
999+ -- Calculate error (difference between gradient+error and quantized color)
1000+ local actual_r = app.pixelColor.rgbaR(targetColor)
1001+ local actual_g = app.pixelColor.rgbaG(targetColor)
1002+ local actual_b = app.pixelColor.rgbaB(targetColor)
1003+
1004+ local err_r = new_r - actual_r
1005+ local err_g = new_g - actual_g
1006+ local err_b = new_b - actual_b
1007+
1008+ -- Distribute error to neighbors
1009+ if px < %d - 1 then
1010+ errors[0][px + 2].r = errors[0][px + 2].r + err_r * 7/16
1011+ errors[0][px + 2].g = errors[0][px + 2].g + err_g * 7/16
1012+ errors[0][px + 2].b = errors[0][px + 2].b + err_b * 7/16
1013+ end
1014+ if py < %d - 1 then
1015+ if px > 0 then
1016+ errors[1][px].r = errors[1][px].r + err_r * 3/16
1017+ errors[1][px].g = errors[1][px].g + err_g * 3/16
1018+ errors[1][px].b = errors[1][px].b + err_b * 3/16
1019+ end
1020+ errors[1][px + 1].r = errors[1][px + 1].r + err_r * 5/16
1021+ errors[1][px + 1].g = errors[1][px + 1].g + err_g * 5/16
1022+ errors[1][px + 1].b = errors[1][px + 1].b + err_b * 5/16
1023+ if px < %d - 1 then
1024+ errors[1][px + 2].r = errors[1][px + 2].r + err_r * 1/16
1025+ errors[1][px + 2].g = errors[1][px + 2].g + err_g * 1/16
1026+ errors[1][px + 2].b = errors[1][px + 2].b + err_b * 1/16
1027+ end
1028+ end
1029+ end
1030+
1031+ img:drawPixel(%d + px, %d + py, targetColor)
1032+ end
1033+ end
1034+ end)
1035+
1036+ spr:saveAs(spr.filename)
1037+ print("Dithering applied successfully")` ,
1038+ layerName , layerName ,
1039+ frameNumber , frameNumber ,
1040+ c1 .R , c1 .G , c1 .B , c1 .A ,
1041+ c2 .R , c2 .G , c2 .B , c2 .A ,
1042+ width , // line 915: error buffer width
1043+ height , // line 920: py loop
1044+ width , // line 925: clear buffer width
1045+ width , // line 930: px loop
1046+ width , // line 933: width check for division by zero
1047+ width , // line 936: gradient calculation
1048+ width , // line 967: right neighbor check
1049+ height , // line 972: bottom neighbor check
1050+ width , // line 981: bottom-right check
1051+ width , // line 1007: right neighbor check (RGB)
1052+ height , // line 1012: bottom neighbor check (RGB)
1053+ width , // line 1021: bottom-right check (RGB)
1054+ x , // line 1031: x coordinate
1055+ y ) // line 1031: y coordinate
1056+ }
0 commit comments