#!/bin/sh # Restart with wish\ exec wish "${0}" "${@}" # TODO: # - Merge into OV # - Add balloon and nameplate # - Make them positionable by drag'n'drop and arrow keys # - Show numeric coordinates in editable panel widgets # - Offer checkboxes to temporarily hide each # - Make key bindings user-configurable, VILE # - Make .aved a toplevel # - Fix size of panel to fit a set number of frames across platforms # - Offer a different look for Windows # - Actually copy images to the user's images directory # - Provide load and save capability # - Do translations # - Make preview background colors configurable # WISHLIST: # - Offer a hierarchial avatar menu # - Offer avatar thumbnails source ov-share.tcl lappend auto_path [file nativename "[pwd]/BWidget-1.3.1"] package require BWidget set MV(homedir) "~/.OpenVerse" # Load images image create photo "img::up" -file "up.gif" image create photo "img::down" -file "down.gif" image create photo "img::new" -file "new.gif" image create photo "img::del" -file "del.gif" image create photo "img::browse" -file "browse.gif" image create photo "img::hand" -file "hand.gif" image create photo "img::yes" -file "yes.gif" image create photo "img::no" -file "no.gif" image create photo "img::play" -file "play.gif" image create photo "img::pause" -file "pause.gif" image create photo "img::file_not_found" -file "file_not_found.gif" image create photo "img::bad_file" -file "bad_file.gif" # TODO: make this user-configurable, VILE; do this at OV start event add <> event add <> event add <> #event add <> #event add <> #event add <> #event add <> event add <> event add <> namespace eval aved { variable num_frames 0 ;# Number of frames in current animation variable sel_frame 0 ;# Index of currently selected frame variable scroll_idx 0 ;# Index of first onscreen frame in list variable vis_frames 7 ;# Number of onscreen frames in list variable frames ;# Dynamic array of frame information variable animated 0 ;# Is this avatar animated? variable bg_color 0 ;# Index of preview color in use variable next_image 0 ;# Counter for next preview image to create variable playback 0 ;# Is the avatar being played right now? variable playback_id ;# After id for playback process variable balloon 0 ;# Is the balloon being shown? variable balloon_x 0 ;# X coordinate of speech balloon variable balloon_y 0 ;# Y coordinate of speech balloon variable name 0 ;# Is the nameplate being shown? variable name_x 0 ;# X coordinate of nameplate variable name_y 0 ;# Y coordinate of nameplate # Default path for loading avatar images variable default_path [file nativename "$MV(homedir)/images"] # List of colors to use for avatar preview background variable colors {"gray" "black" "white" "navy"} # --- ANIMATION EDITOR -------------------------------------------------------- # Highlight frame $idx with $color proc set_frame_color {idx color} { foreach elem {".hand" ".num" ".img" ".dur" ".ms" ""} { .aved.anim.${idx}$elem configure -bg $color } } # Show selection (or lack thereof, depending on $flag) for frame $idx proc show_selection {idx flag} { variable sel_frame # This uses a dirty hack to hide or show the hand... set tmp $sel_frame if $flag { set sel_frame $idx verify_image $idx -trust-cache set_frame_color $idx lightblue } else { set sel_frame -1 verify_image $idx -trust-cache set_frame_color $idx gray } set sel_frame $tmp } # Make $idx the active frame proc select_frame {idx args} { variable num_frames variable frames variable scroll_idx variable sel_frame # Clip $idx to available frames if {$idx < 0} { set idx 0 } elseif {$idx >= $num_frames} { set idx [expr $num_frames - 1] } # Scroll to the frame if necessary scroll_to $idx # Adjust focus (focus handler will do the rest) set cur_focus [focus -displayof .] if [string match ".aved.anim.*" $cur_focus] { set part [string range $cur_focus [expr [string last .\ $cur_focus] + 1] end] } else { set part "img" } if {[string_equal ".aved.anim.$sel_frame.$part" $cur_focus] && [string match [lindex $args 0] "-force"]} { handle_focus $idx } else { focus .aved.anim.$idx.$part } } # Handle focus on frame in visible slot $idx proc handle_focus {idx} { variable num_frames variable sel_frame variable scroll_idx variable vis_frames variable frames # Update preview image verify_image $idx -vcmd $frames($idx.img) # Adjust colors and hand icon for current and previous frames if {$sel_frame != $idx} { if {$sel_frame < $num_frames} {show_selection $sel_frame 0} show_selection $idx 1 set sel_frame $idx } } # Add a new frame widget to the list proc create_frame_widget {idx} { variable sel_frame # Create the gui elements set path .aved.anim.$idx frame $path -bd 1 -relief sunken label $path.hand -bd 0 -width 0 entry $path.img -highlightthickness 0 -bd 0 -width 1 -validate key\ -vcmd "aved::verify_image $idx -vcmd %P" -textvariable\ "aved::frames($idx.img)" entry $path.dur -highlightthickness 0 -bd 0 -width 5 -justify right\ -textvariable "aved::frames($idx.dur)" -vcmd\ "aved::set_button_states %P" -validate key label $path.num -bd 0 -justify right -width 0 -text "[expr $idx + 1]."\ -font [$path.img cget -font] label $path.ms -bd 0 -text "ms" -width 2 -font [$path.img cget -font] bind $path "aved::handle_focus $idx" show_selection $idx [expr $idx == $sel_frame] verify_image $idx -trust-cache # Put together the frame pack $path.hand $path.num -side left pack $path.img -side left -fill x -expand 1 pack $path.dur $path.ms -side left # Pack it if {$idx == 0} { pack $path -fill x } else { pack $path -fill x -after .aved.anim.[expr $idx - 1] } } # Insert a new frame immediately after active frame # Also used to initialize the very first frame proc new_frame {args} { variable num_frames variable sel_frame variable frames variable scroll_idx variable vis_frames variable next_image variable animated if [string_equal $args "-init"] { set idx 0 set frames($idx.dur) 100 } else { set idx [expr $sel_frame + 1] for {set i $num_frames} {$i > $idx} {incr i -1} { set j [expr $i - 1] foreach elem {"img" "dur" "good" "img_id"} { set frames($i.$elem) $frames($j.$elem) } if {$i < $num_frames} {verify_image $i -trust-cache} } set frames($idx.dur) $frames($sel_frame.dur) } set frames($idx.img) "" set frames($idx.good) 0 set frames($idx.img_id) "img::aved::$next_image" image create photo $frames($idx.img_id) incr next_image incr num_frames # Create, configure, and pack the gui elements as needed if $animated { create_frame_widget [expr $num_frames - 1] select_frame $idx } } # Delete active frame proc del_frame {} { variable num_frames variable sel_frame variable frames if {$num_frames == 1} {return} # Shift frames back to cover deleted one set img $frames($sel_frame.img_id) for {set i $sel_frame} {$i < [expr $num_frames - 1]} {incr i} { set j [expr $i + 1] foreach elem {"img" "dur" "good" "img_id"} { set frames($i.$elem) $frames($j.$elem) } verify_image $i -trust-cache } # Destroy final frame widget and the no-longer-needed image destroy .aved.anim.[expr $num_frames - 1] image delete $img # Remove array entry for final frame incr num_frames -1 array unset frames "$num_frames.*" # Highlight new current frame select_frame $sel_frame -force } # Swap the active frame and its $adj'th neighbor proc move_frame {adj} { variable num_frames variable sel_frame variable frames set j [expr $sel_frame + $adj] if {$j < 0 || $j >= $num_frames} {return} foreach elem {"img" "dur" "img_id"} { swap frames($sel_frame.$elem) frames($j.$elem) } select_frame $j } # Display frame $idx proc scroll_to {idx args} { variable num_frames variable vis_frames variable scroll_idx set do_scroll 1 if [string_equal $args "-abs"] {set abs 1} else {set abs 0} # Keep $idx in range if {$idx < 0} { set idx 0 } elseif $abs { if {$num_frames >= $vis_frames} { if [expr $idx > $num_frames - $vis_frames &&\ $num_frames >= $vis_frames] { set idx [expr $num_frames - $vis_frames] } } else { set idx 0 } } elseif {$idx >= $num_frames} { set idx [expr $num_frames - 1] } # Is this frame already visible? if $abs { if {$scroll_idx == $idx} {set do_scroll 0} } else { if [expr $idx >= $scroll_idx && $idx < $scroll_idx +\ $vis_frames] { if [expr $scroll_idx + $vis_frames > $num_frames &&\ $num_frames >= $vis_frames] { # Eliminate dead space at end of list set idx [expr $num_frames - $vis_frames] } else { set do_scroll 0 } } } if $do_scroll { # Calculate some stuff if {$idx < $scroll_idx} { # Scroll up set add [expr $scroll_idx - 1] set del [expr $scroll_idx + $vis_frames - 1] set count [expr $scroll_idx - $idx] set scroll_idx $idx set adj -1 set offset "-before" } else { # Scroll down set add [expr $scroll_idx + $vis_frames] set del $scroll_idx if $abs { set count [expr $idx - $scroll_idx] set scroll_idx $idx } else { set count [expr 1 + $idx - $scroll_idx -\ $vis_frames] set scroll_idx [expr $idx - $vis_frames + 1] } set adj 1 set offset "-after" } # Scroll one frame at a time for {set i 0} {$i < $count} {incr i} { pack forget .aved.anim.$del pack .aved.anim.$add -fill x\ $offset .aved.anim.[expr $add - $adj] incr del $adj incr add $adj } } # Fix the scrollbar .aved.scroll set [expr $scroll_idx / ${num_frames}.0]\ [expr ($scroll_idx + $vis_frames) / ${num_frames}.0] } # Handle scrollbar movement proc scroll {args} { variable num_frames variable scroll_idx variable vis_frames switch -- [lindex $args 0] { "moveto" { scroll_to [expr int($num_frames * [lindex $args 1])] -abs } "scroll" { set adj [lindex $args 1] switch -- [lindex $args 2] { "pages" { scroll_to [expr $scroll_idx + $adj * $vis_frames] -abs } "units" { scroll_to [expr $scroll_idx + $adj] -abs }} }} } # Start or stop playback of animation proc toggle_play {} { variable sel_frame variable playback variable playback_id variable frames if $playback { set playback 0 .aved.funcs.anim.play configure -image "img::play" after cancel $playback_id .aved.preview itemconfigure img -image\ $frames($sel_frame.img_id) } else { set playback 1 .aved.funcs.anim.play configure -image "img::pause" next_frame 0 } } # Go to the next frame of an animation proc next_frame {idx} { variable frames variable playback_id variable num_frames # Show current frame if {$idx >= $num_frames} {set idx 0} .aved.preview itemconfigure img -image $frames($idx.img_id) update # Arrage for next frame to be shown set playback_id [after $frames($idx.dur) "aved::next_frame\ [expr $idx + 1]"] } # Create the animation panel proc create_anim_panel {} { variable frames frame .aved.funcs.anim frame .aved.funcs.anim.buttons foreach {elem func} {"up" "move_frame -1" "down" "move_frame 1" "new" "new_frame" "del" "del_frame" "browse" "pick_image" "play" "toggle_play"} { button .aved.funcs.anim.$elem -image "img::$elem" -bd 1\ -highlightthickness 0 -command "aved::$func" pack .aved.funcs.anim.$elem -in .aved.funcs.anim.buttons\ -side top -fill y -expand 1 } scrollbar .aved.scroll -highlightthickness 0 -bd 0 -relief flat\ -orient vertical -command {aved::scroll}\ -elementborderwidth 1 pack .aved.scroll -in .aved.funcs.anim -side left -fill y pack .aved.funcs.anim.buttons -side right -fill y -expand 1 frame .aved.anim create_frame_widget 0 } # Display the animation editor panel proc show_anim_panel {} { pack .aved.funcs.anim -side left -fill y pack .aved.anim -in .aved.edit -side left -expand 1 -fill both select_frame 0 # Some key bindings... # TODO: make these bind .aved bind . <> {aved::select_frame [expr $aved::sel_frame - 1]} bind . <> {aved::select_frame [expr $aved::sel_frame + 1]} bind . <> {aved::select_frame 0} bind . <> {aved::select_frame [expr $aved::num_frames - 1]} bind . <> {aved::scroll scroll -1 pages} bind . <> {aved::scroll scroll 1 pages} bind . <> {aved::new_frame} bind . <> {aved::del_frame} foreach ev {"Prev" "Next" "First" "Last" "PgUp" "PgDn" "New" "Del"} { bind . <> +break } } # Hide the animaion panel proc hide_anim_panel {} { variable sel_frame variable scroll_idx variable num_frames variable vis_frames variable playback if $playback {toggle_play} pack forget .aved.funcs.anim pack forget .aved.anim foreach ev {"Prev" "Next" "First" "Last" "PgUp" "PgDn" "New" "Del"} { bind . <> } } # --- STILL EDITOR ------------------------------------------------------------ # Create still avatar editor panel proc create_still_panel {} { frame .aved.still label .aved.still.image -text "GIF Filename:" frame .aved.still.edit -bd 1 -relief sunken entry .aved.still.edit.img -highlightthickness 0 -bd 0\ -textvariable "aved::frames(0.img)" -validate key\ -vcmd "aved::verify_image 0 -vcmd %P" button .aved.still.edit.browse -image "img::browse" -bd 1\ -highlightthickness 0 -command "aved::pick_image" pack .aved.still.edit.img -side left -fill both -expand 1 pack .aved.still.edit.browse -side right pack .aved.still.image .aved.still.edit -fill x -expand 1 } # Display the still panel proc show_still_panel {} { pack .aved.still -in .aved.edit -fill x -expand 1 bind . <> {aved::pick_image; break} focus .aved.still.edit.img .aved.still.edit.img icursor end } # Hide the still panel proc hide_still_panel {} { pack forget .aved.still bind . <> } # --- COMMON ------------------------------------------------------------------ # Create common AvEd panel # TODO: offer a slightly different look tailored for Windows # (make this a global OV setting, perhaps, along with colors 'n stuff) proc create_common_panel {} { variable colors variable bg_color # Background colors frame .aved.bg label .aved.bg.bg -text "BG:" -bd 0 pack .aved.bg.bg -side left -fill x -expand 1 set i 0 foreach {name} $colors { radiobutton .aved.bg.$name -text [string totitle $name]\ -command "aved::set_background" -value $i\ -variable "aved::bg_color" -bd 1 pack .aved.bg.$name -side left -fill x -expand 1 incr i } # Preview canvas frame .aved.pframe -bd 1 -relief sunken canvas .aved.preview -highlightthickness 0 -bd 0 -width 0 -height 0 .aved.preview create image 0 0 -tag img set_background bind .aved.preview "aved::adjust_preview" pack .aved.preview -in .aved.pframe -fill both -expand 1 # Editor pane # TODO: fix this filthy hack with the widget pady stuff frame .aved.edit -height 108 frame .aved.overlay -bd 1 -relief sunken foreach elem {"Balloon" "Name"} { set var "aved::[string tolower $elem]" set path ".aved.overlay.[string tolower $elem]" frame $path -bd 1 -relief raised frame $path.f checkbutton $path.b -text $elem -variable $var -bd 1 label $path.l -text "(" entry $path.x -textvariable "${var}_x" -width 3 -bd 0\ -highlightthickness 0 -justify right label $path.c -text "," entry $path.y -textvariable "${var}_y" -width 3 -bd 0\ -highlightthickness 0 -justify right label $path.r -text ")" foreach elem2 {"b" "l" "x" "c" "y" "r"} { pack $path.$elem2 -in $path.f -side left } pack $path.f -expand 1 pack $path -side left -fill x -expand 1 } pack .aved.overlay -in .aved.edit -fill x -expand 1 frame .aved.funcs -bd 1 -relief sunken frame .aved.buttons foreach {type name func} { "button" "load" "" "button" "save" "" "button" "close" "exit" "checkbutton" "anim" "aved::toggle_anim"} { $type .aved.buttons.$name -text [string totitle $name]\ -command $func -highlightthickness -0 -bd 1\ -padx 4 -pady 8 pack .aved.buttons.$name -side top -fill both -expand 1 } .aved.buttons.anim configure -pady 3 .aved.buttons.anim configure -variable "aved::animated" pack .aved.buttons -in .aved.funcs -side right -fill y pack .aved.funcs -in .aved.edit -side right -fill y } # Show the common parts of the avatar editor panel proc show_common_panel {} { variable frames variable sel_frame .aved.preview itemconfigure img -image $frames($sel_frame.img_id) pack .aved.bg -fill x pack .aved.pframe -fill both -expand 1 pack .aved.edit -fill x } # Switch between still and animated avatar mode proc toggle_anim {} { variable animated variable frames if $animated { hide_still_panel show_anim_panel } else { select_frame 0 hide_anim_panel show_still_panel } } # Change the color of the preview canvas proc set_background {} { variable bg_color variable colors .aved.preview configure -bg [lindex $colors $bg_color] } # Relocate the preview contents to be in the canvas center # TODO: move balloon and nameplate proc adjust_preview {} { set x1 [lindex [.aved.preview coords img] 0] set y1 [lindex [.aved.preview coords img] 1] set x2 [expr [winfo width .aved.preview] / 2] set y2 [expr [winfo height .aved.preview] / 2] .aved.preview move img [expr $x2 - $x1] [expr $y2 - $y1] } # Allow user to pick a file from a browser; save as active frame proc pick_image {} { variable sel_frame variable frames variable default_path variable animated set file [tk_getOpenFile -initialdir $default_path -filetypes { {"GIF Image Files" {.gif .GIF .Gif}} {"GIF Image Files" {} "GIFF" } {"All Files" * }}] if ![string length $file] {return} # Replace spaces with underscores set newname [string_map {" " "_"} [file tail $file]] # TODO: enable this # Copy the selected file to the user's images directory, if necessary #if {![file exists "$MV(homedir)/images/$newname"] || # [file size "$MV(homedir)/images/$newname"] != # [file size $file]} { # file copy -force $file "$MV(homedir)/images/$newname" #} set default_path [file dirname $file] set frames($sel_frame.img) $newname if $animated { set widget ".aved.anim.$sel_frame.img" } else { set widget ".aved.still.edit.img" } focus $widget $widget icursor end } # Checks the "good" cache to determine whether to enable save and playback proc set_button_states {args} { variable frames variable num_frames variable sel_frame variable animated variable playback set save_state "normal" for {set idx 0} {$idx < $num_frames} {incr idx} { if {$frames($idx.good) != 3} { set save_state "disabled" break } } if $animated { if [llength $args] { if {[string is digit -strict [lindex $args 0]] && [lindex $args 0] >= 10} { set test "\$idx != $sel_frame &&\ \$frames(\$idx.dur) < 1" } else { set test "\$idx == $sel_frame ||\ \$frames(\$idx.dur) < 1" } } else { set test "\$frames(\$idx.dur) < 1" } set anim_state "normal" for {set idx 0} {$idx < $num_frames} {incr idx} { if $test { set save_state "disabled" set anim_state "disabled" break } } if {$playback && [string_equal $anim_state "disabled"]} { toggle_play } .aved.funcs.anim.play configure -state $anim_state } .aved.buttons.save configure -state $save_state if [llength $args] {return [string is digit [lindex $args 0]]} } # Verifies $idx's vailidity and sets its hand element to reflect proc verify_image {idx args} { global MV variable sel_frame variable frames variable playback variable num_frames set update_cache 1 set vcmd 0 # Handle arguments if [llength $args] { switch -- [lindex $args 0] { case "-trust-cache" { set update_cache 0 } case "-vcmd" { set vcmd 1 }} } # Determine a "good" value to cache, or just use the old value? # "good" values: 0: no name; 1: file not found; 2: bad file; 3: success if $update_cache { # Determine image filename if $vcmd { set orig_name [lindex $args 1] } else { set orig_name $frames($idx.img) } set name "$MV(homedir)/images/[string_map {" " "_"} $orig_name]" if $vcmd { # Assumption: in vcmd mode, idx == sel_frame # Therefore, modify frame image set good [load_image $idx $name $orig_name] } else { # Assumption: when not in vcmd mode, idx != sel_frame # except to initially show the hand and frame images # Therefore, modify the hand if {[string_equal -nocase [file extension $name]\ ".gif"] && [file readable $name]} { set good 3 } elseif ![string length $orig_name] { set good 0 } else { set good 1 } } } set frames($idx.good) $good # Now make use of the cached information if !$vcmd { # Nothing more to do for vcmd mode... if {$idx == $sel_frame} { set img "img::hand" if !$playback { .aved.preview itemconfigure img -image\ $frames($idx.img_id) } } elseif {$frames($idx.good) == 3} { set img "img::yes" } else { set img "img::no" } .aved.anim.$idx.hand configure -image $img } # Enable or disable the save and playback buttons set_button_states if $vcmd { # Appease the calling entry field return 1 } } # Attempt to load an image from disk; return a "good" value proc load_image {idx name orig_name} { variable frames if [string length $orig_name] { if [file readable $name] { $frames($idx.img_id) blank if {![string_equal -nocase [file extension $name]\ ".gif"] || [catch "$frames($idx.img_id)\ read $name -shrink"]} { $frames($idx.img_id) copy "img::bad_file"\ -shrink return 2 } return 3 } else { $frames($idx.img_id) copy "img::file_not_found" -shrink return 1 } } else { $frames($idx.img_id) blank return 0 } } # Main AvEd procedure proc main {} { # TODO: make .aved a toplevel, not a frame wm title . "OpenVerse Avatar Editor" wm resizable . 1 1 wm minsize . 300 330 frame .aved #new_frame -init #create_common_panel #create_still_panel #create_anim_panel #show_still_panel #show_common_panel #pack .aved -expand 1 -fill both frame .colors frame .preview -relief sunken -bd 1 frame .edit PanedWindow .div -side left set path [.div add] pack .colors -in $path -side top -fill x pack .preview -in $path -side bottom -fill both -expand 1 set path [.div add] pack .edit -in $path -fill both -expand 1 pack .div -fill both -expand 1 update # balloon::do_balloon 40 40 .aved.preview balloon -text "Hello!" } } aved::main