{"id":217,"date":"2025-04-13T07:09:27","date_gmt":"2025-04-13T07:09:27","guid":{"rendered":"https:\/\/www.mickiwiki.com\/?p=217"},"modified":"2025-04-13T09:37:13","modified_gmt":"2025-04-13T09:37:13","slug":"drag-to-make-gif","status":"publish","type":"post","link":"https:\/\/www.mickiwiki.com\/?p=217","title":{"rendered":"drag to make gif"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"551\" src=\"https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-1024x551.png\" alt=\"\" class=\"wp-image-218\" srcset=\"https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-1024x551.png 1024w, https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-300x161.png 300w, https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-768x413.png 768w, https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-1536x827.png 1536w, https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/DragtoGif-2048x1102.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Loads images. Makes thumbnails in top bit. Drag images below line. Makes a gif of images below line in left to right order. Y-axis doesn&#8217;t matter. Can load same images more than once. Can quickly check movement. Gif saved to &#8220;output.gif&#8221;. Image sequence .CSV files saved to .\/LISTS\/image_list_*. <\/p>\n\n\n\n<div class=\"wp-block-file\"><a id=\"wp-block-file--media-0e082d6e-df9b-4580-9a72-7dbe3daa14da\" href=\"https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/drag_to_gif.7z\">drag_to_gif<\/a><a href=\"https:\/\/www.mickiwiki.com\/wp-content\/uploads\/2025\/04\/drag_to_gif.7z\" class=\"wp-block-file__button wp-element-button\" download aria-describedby=\"wp-block-file--media-0e082d6e-df9b-4580-9a72-7dbe3daa14da\">Download<\/a><\/div>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/perl\nuse strict;\nuse warnings;\nuse Tk;\nuse Tk::Canvas;\nuse Tk::Scrollbar;\nuse File::Basename;\nuse File::Copy;\nuse File::Path qw(remove_tree);\nuse Image::Imlib2;\nuse Tk::PNG;\nuse Tk::JPEG;\nuse Tk::Photo;\nuse feature \"say\";\nuse Data::Dumper qw(Dumper);\nmy ($gif,$canvas2,$frame,$window2,$max_frames,$image_item,$ww,$wh,$counter);\nmy $number=0;\nmy @saved;\nmy $mw = MainWindow-&gt;new;\n$mw-&gt;title(\"Drag &amp; Re-order Images\");\nmy $button_frame = $mw-&gt;Frame(-bg =&gt; 'lightgray')-&gt;pack(-fill =&gt; 'x', -side =&gt; 'top');\nmy @lists;\nmy $chk=0;\n$button_frame-&gt;Button(\n    -text    =&gt; \"Load Images\",\n    -command =&gt; \\&amp;load_images,\n    -font    =&gt; &#91;'Arial', 14],\n    -width   =&gt; 15,\n    -height  =&gt; 2\n)-&gt;pack(-side =&gt; 'left', -padx =&gt; 10);\n\n$button_frame-&gt;Button(\n    -text    =&gt; \"NEW GIF\",\n    -command =&gt; sub{&amp;rename_images;&amp;make_gif;&amp;play_gif;&amp;remove;},\n    -font    =&gt; &#91;'Arial', 14],\n    -width   =&gt; 15,\n    -height  =&gt; 2\n)-&gt;pack(-side =&gt; 'left', -padx =&gt; 10);\n\n$button_frame-&gt;Button(\n    -text    =&gt; \"CLOSE\",\n    -command =&gt; sub{&amp;exit},\n    -font    =&gt; &#91;'Arial', 14],\n    -width   =&gt; 15,\n    -height  =&gt; 2\n)-&gt;pack(-side =&gt; 'left', -padx =&gt; 10);\n\n# Main canvas frame (contains both scrollable areas)\nmy $canvas_frame = $mw-&gt;Frame()-&gt;pack(-fill =&gt; 'both', -expand =&gt; 1);\nmy $canvas = $canvas_frame-&gt;Canvas(-width =&gt; 2000, -height =&gt; 500, -bg =&gt; 'white');\nmy $scroll_x = $canvas_frame-&gt;Scrollbar(-orient =&gt; 'horizontal', -command =&gt; sub { $canvas-&gt;xview(@_) });\n$canvas-&gt;configure(-xscrollcommand =&gt; &#91;'set', $scroll_x]);\n$canvas-&gt;pack(-side =&gt; 'top', -fill =&gt; 'both', -expand =&gt; 1);\n$scroll_x-&gt;pack(-side =&gt; 'bottom', -fill =&gt; 'x');\n\n\n# Separation line\nmy $separation_y = 220;\nmy $separation_line = $canvas-&gt;createLine(0, $separation_y, 10000, $separation_y, -fill =&gt; 'black', -width =&gt; 2);\n\nmy %image_data;\nmy $dragging;\nmy ($drag_offset_x, $drag_offset_y);\nMainLoop;\n\nsub load_images {\n    my @files = @{$mw-&gt;getOpenFile(\n        -multiple =&gt; 1,\n        -filetypes =&gt; &#91;&#91;'Images', &#91;'.png', '.jpg', '.jpeg', '.gif']]],\n    )};\n    return unless @files;\n #say Dumper (\\@files);   \n    my $x = 10;\n    my $max_x = 10;\n    \n    foreach my $file (@files) {\n        next unless -f $file;\n        \n        my $img = create_thumbnail($file);\n        my $id = $canvas-&gt;createImage($x, 10, -image =&gt; $img, -anchor =&gt; 'nw');\n        \n        $image_data{$id} = { file =&gt; $file, x =&gt; $x, y =&gt; 10, image =&gt; $img };\n        \n        # Enable dragging\n        $canvas-&gt;bind($id, '&lt;ButtonPress-1&gt;' =&gt; &#91;\\&amp;start_drag, $id]);\n        $canvas-&gt;bind($id, '&lt;B1-Motion&gt;' =&gt; &#91;\\&amp;do_drag, $id]);\n        $canvas-&gt;bind($id, '&lt;ButtonRelease-1&gt;' =&gt; &#91;\\&amp;stop_drag, $id]);\n        \n        $x += 100; # check this\n        $max_x = $x if $x &gt; $max_x;\n    }\n    \n    # Update scroll region dynamically\n    $canvas-&gt;configure(-scrollregion =&gt; &#91;0, 0, $max_x, 800]); # need this\n\n}\n\n\nsub create_thumbnail {\n    my ($file) = @_;\n    my $img = $mw-&gt;Photo(-file =&gt; $file);\n    my ($width, $height) = ($img-&gt;width, $img-&gt;height);\n($ww,$wh)=($width,$height); \n    my $scale = 80 \/ $width;\n    my $thumb = $mw-&gt;Photo(-width =&gt; 80, -height =&gt; int($height * $scale));\n    $thumb-&gt;copy($img, -subsample =&gt; int($width \/ 80));\n\n    return $thumb;\n}\n\nsub start_drag {\n    my ($canvas, $id) = @_;\n    $dragging = $id;\n    my $e = $canvas-&gt;XEvent;\n    my ($img_x, $img_y) = $canvas-&gt;coords($id);\n    $drag_offset_x = $e-&gt;x - $img_x;\n    $drag_offset_y = $e-&gt;y - $img_y;\n}\n\nsub do_drag {\n    my ($canvas, $id) = @_;\n    return unless $dragging;\n    \n    my $e = $canvas-&gt;XEvent;\n    my $new_x = $e-&gt;x - $drag_offset_x;\n    my $new_y = $e-&gt;y - $drag_offset_y;\n    \n    $canvas-&gt;coords($id, $new_x, $new_y);\n    \n    # Ensure items can move across the separation line\n    $image_data{$id}{x} = $new_x;\n    $image_data{$id}{y} = $new_y;\n}\n\nsub stop_drag {\n    $dragging = undef;\n}\n\n\nsub rename_images {\n$chk++;\nif($window2){$window2-&gt;destroy;}\nmy $dir = \"re-ordered\";  #added\n    my @sorted_images = sort { \n        my $a_x = $image_data{$a}{x} \/\/ 0; \n        my $b_x = $image_data{$b}{x} \/\/ 0;\n        $a_x &lt;=&gt; $b_x\n    }\n    grep { \n        my $y = $image_data{$_}{y} \/\/ 0; \n        $y &gt; $separation_y\n    } keys %image_data;\n   \n    return unless @sorted_images;\n\n  $counter =0;\n    for my $id (@sorted_images) {\n        \n        my ($old_path, $ext) = ($image_data{$id}{file}, '.png');\n#say $old_path;\n$lists&#91;$chk]&#91;$counter]{file}=$old_path;\n#say Dumper(\\@lists);\nmy $new_path = sprintf(\"re-ordered\/image_%04d\".\"$ext\",$counter);\n        copy($old_path, $new_path);\n       # print \"Renamed: $old_path -&gt; $new_path\\n\";\n$counter++;\n    }\n\n}\n\n\n\nsub play_gif{\n    my ($ww, $wh) = ($ww + 60, $wh + 60);\n    $window2 = $mw-&gt;Toplevel;\n    $window2-&gt;geometry(\"${ww}x${wh}+1500+500\");\n    $canvas2 = $window2-&gt;Canvas(-width =&gt; $ww, -height =&gt; $wh);\n    $canvas2-&gt;pack;\n\n    my $myfile = 'output.gif';\n    $gif = $mw-&gt;Photo(-file =&gt; $myfile, -format =&gt; 'gif');\n\n    # Auto-detect number of frames\n    my $frames = 0;\n    while (eval { $gif-&gt;configure(-format =&gt; \"gif -index $frames\"); 1 }) {\n        $frames++;\n    }\n\n    if ($frames == 0) {\n        print \"Error: No frames found in GIF!\\n\";\n        return;\n    }\n    \n    $frame = 0;\n    $max_frames = $frames;  # Use detected frame count\n\n    $image_item = $canvas2-&gt;createImage(0, 0, -anchor =&gt; 'nw', -image =&gt; $gif);\n\n    sub update_frame {\n        $gif-&gt;configure(-format =&gt; \"gif -index $frame\");\n        $canvas2-&gt;itemconfigure($image_item, -image =&gt; $gif);\n        $frame = ($frame + 1) % $max_frames;\n        $window2-&gt;after(100, \\&amp;update_frame);\n    }\n\n    update_frame();\n}\t\n\t\nsub remove {\nmy %temp;\nmy $dir = \"LISTS\";\nif (! -d $dir){mkdir \"LISTS\"}\n\nmy $file_name = sprintf(\"image_list_%04d\",$chk);\nopen(my $fh, '&gt;', \"$dir\/$file_name\") or die \"Cannot open filelist: $!\";\nfor (my $i=0;$i&lt;=$counter-1;$i++){\n$temp{file} = $lists&#91;$chk]&#91;$i]{file}; \n#say \" temp = $temp\";\t\nprint $fh \"image,$temp{file}\\n\";\n\t}\nclose $fh;\t\n    $dir = \"re-ordered\";\n    if (-d $dir) {\n        remove_tree($dir, { error =&gt; \\my $err });\n        if (@$err) {\n            warn \"Error removing directory: @$err\\n\";\n        } else {\n            #print \"Removed directory: $dir\\n\";\n        }\n    }\n    mkdir $dir;  # Recreate it for new images\n\n}\n\nsub numerically {\n    my ($a_num) = $a =~ \/(\\d+)\/;\n    my ($b_num) = $b =~ \/(\\d+)\/;\n$a_num \/\/= 0;  # Set $a_num to 0 if it's uninitialized\n$b_num \/\/= 0;  # Set $b_num to 0 if it's uninitialized\nreturn $a_num &lt;=&gt; $b_num;\n}\n\n\n\nsub make_gif{ \n# add check for directories and files.\nunless (-e \"output.gif\") {\n    open my $fh, '&gt;', \"output.gif\" or die \"Cannot create output.gif: $!\";\n    close $fh;\n}\n\nsystem('ffmpeg -y -v quiet -i re-ordered\/image_%04d.png -vf \"palettegen\" palette.png');\nsystem('ffmpeg -y -v quiet -framerate 10 -i re-ordered\/image_%04d.png -i palette.png -filter_complex \"paletteuse\" -gifflags -offsetting -loop 0 output.gif');\n}\n\nsub close{\n$mw-&gt;destroy;\t\n\t}\n\n<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Loads images. Makes thumbnails in top bit. Drag images below line. Makes a gif of images below line in left to right order. Y-axis doesn&#8217;t matter. Can load same images more than once. Can quickly check movement. Gif saved to &#8220;output.gif&#8221;. Image sequence .CSV files saved to .\/LISTS\/image_list_*.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"neve_meta_sidebar":"","neve_meta_container":"","neve_meta_enable_content_width":"","neve_meta_content_width":0,"neve_meta_title_alignment":"","neve_meta_author_avatar":"","neve_post_elements_order":"","neve_meta_disable_header":"","neve_meta_disable_footer":"","neve_meta_disable_title":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-217","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/posts\/217","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=217"}],"version-history":[{"count":4,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/posts\/217\/revisions"}],"predecessor-version":[{"id":225,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=\/wp\/v2\/posts\/217\/revisions\/225"}],"wp:attachment":[{"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=217"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=217"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mickiwiki.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=217"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}