![]() |
<!DOCTYPE HTML> <HTML LANG="ja"> <HEAD> <META CHARSET="Shift_JIS"> <TITLE>GIF アニメーション</TITLE> </HEAD> <BODY STYLE="background-color:#CCFFFF"> <DIV STYLE="text-align:center"> <B>GIF アニメーション</B> <BR><BR> <FORM> <DIV STYLE="position:relative; display:inline-block; text-align:left; white-space:nowrap"> 動画ファイル:<BR> <INPUT TYPE=FILE ID="file" SIZE=60 onChange="sel_file()"> </DIV> <BR><BR> <DIV STYLE="position:relative; display:inline-block"> <VIDEO ID="vid" CONTROLS STYLE="position:relative; background-color:black"></VIDEO> <IFRAME ID="gif" STYLE="position:absolute; left:0; top:0; border:0; visibility:hidden"></IFRAME> </DIV> <BR><BR> <TABLE STYLE="margin-left:auto; margin-right:auto; text-align:left; white-space:nowrap"> <TR><TD> 開始位置: <INPUT TYPE=TEXT ID="fr_h" SIZE=2 STYLE="text-align:right"> 時間 <INPUT TYPE=TEXT ID="fr_m" SIZE=2 STYLE="text-align:right"> 分 <INPUT TYPE=TEXT ID="fr_s" SIZE=2 STYLE="text-align:right"> 秒 <INPUT TYPE=TEXT ID="fr_ms" SIZE=3 STYLE="text-align:right"> ms <INPUT TYPE=BUTTON ID="fr_cur" VALUE="現在位置" DISABLED onClick="set_cur(0)" STYLE="padding-left:0.1em; padding-right:0.1em"> </TD></TR> <TR><TD> 終了位置: <INPUT TYPE=TEXT ID="to_h" SIZE=2 STYLE="text-align:right"> 時間 <INPUT TYPE=TEXT ID="to_m" SIZE=2 STYLE="text-align:right"> 分 <INPUT TYPE=TEXT ID="to_s" SIZE=2 STYLE="text-align:right"> 秒 <INPUT TYPE=TEXT ID="to_ms" SIZE=3 STYLE="text-align:right"> ms <INPUT TYPE=BUTTON ID="to_cur" VALUE="現在位置" DISABLED onClick="set_cur(1)" STYLE="padding-left:0.1em; padding-right:0.1em"> </TD></TR> <TR><TD> フレーム間隔: <INPUT TYPE=TEXT ID="int" SIZE=4 STYLE="text-align:right"> ×10ms </TD></TR> <TR><TD> ループ再生: <LABEL><INPUT TYPE=RADIO ID="lp_never" NAME="loop" CHECKED> しない</LABEL> <LABEL><INPUT TYPE=RADIO NAME="loop" STYLE="margin-left:0.5em"> 無限</LABEL> <LABEL><INPUT TYPE=RADIO ID="lp_cnt" NAME="loop" STYLE="margin-left:0.5em"> </LABEL><INPUT TYPE=TEXT ID="lp_times" SIZE=4 STYLE="text-align:right"><LABEL FOR="lp_cnt"> 回</LABEL> </TD></TR> <TR><TD> <BR> <INPUT TYPE=BUTTON ID="cre" VALUE="作成" DISABLED onClick="create_gif()"> <SPAN ID="sts" STYLE="margin-left:1em"></SPAN> </TD></TR> <TR><TD> フレーム: <SPAN ID="frms" STYLE="display:inline-block; min-width:2.5em"></SPAN> <SPAN STYLE="margin-left:1em">時間:</SPAN> <SPAN ID="time" STYLE="display:inline-block; min-width:8em"></SPAN> </TD></TR> <TR><TD> <BR> <INPUT TYPE=BUTTON ID="view" VALUE="表示" DISABLED onClick="view_gif()"> <INPUT TYPE=BUTTON ID="cls" VALUE="表示終了" DISABLED onClick="close_gif()" STYLE="margin-left:2em"> <INPUT TYPE=BUTTON ID="save" VALUE="保存" DISABLED onClick="save_gif()" STYLE="margin-left:2em"> </TD></TR> </TABLE> </FORM> </DIV> <A ID="dl" STYLE="display:none"></A><!--保存処理用--> <SCRIPT TYPE="text/javascript"> <!-- function sel_file() { var file = elem_file.files.item(0); if(elem_vid.src != "") URL.revokeObjectURL(elem_vid.src); elem_vid.src = URL.createObjectURL(file); elem_vid.load(); file_name = file.name; close_gif(); elem_gif.src = ""; elem_cre.disabled = true; elem_sts.textContent = elem_frms.textContent = elem_time.textContent = ""; elem_view.disabled = true; elem_save.disabled = true; if(gif_img != undefined) { URL.revokeObjectURL(gif_page); URL.revokeObjectURL(gif_img); gif_img = undefined; } } function vid_canplay() { if(!elem_vid.videoWidth) { vid_error(); return; } elem_fr_cur.disabled = elem_to_cur.disabled = false; elem_cre.disabled = false; } function vid_error() { frames = undefined; elem_file.disabled = false; elem_vid.controls = true; elem_gif.src = ""; elem_fr_cur.disabled = elem_to_cur.disabled = true; elem_cre.value = "作成"; elem_sts.textContent = elem_frms.textContent = elem_time.textContent = ""; elem_view.disabled = true; elem_save.disabled = true; if(gif_img != undefined) { URL.revokeObjectURL(gif_page); URL.revokeObjectURL(gif_img); gif_img = undefined; } alert("動画を再生できません。"); } // 開始位置/終了位置にビデオの現在位置を設定 function set_cur(i) { var time = elem_vid.currentTime; var int = Math.floor(time); elem_ms[i].value = String(Math.floor((time - int) * 1000)); var rem = int % 60; elem_s[i].value = String(rem); int = (int - rem) / 60; if(int) { rem = int % 60; elem_m[i].value = String(rem); int = (int - rem) / 60; elem_h[i].value = (int) ? String(int) : ""; } else { elem_h[i].value = elem_m[i].value = ""; } } function lp_times_click() { elem_lp_cnt.click(); } // 作成/中止 function create_gif() { if(frames != undefined) { // 作成中 cancel = true; return; } var i; close_gif(); elem_file.disabled = true; elem_gif.src = ""; elem_sts.textContent = elem_frms.textContent = elem_time.textContent = ""; elem_view.disabled = true; elem_save.disabled = true; if(gif_img != undefined) { URL.revokeObjectURL(gif_page); URL.revokeObjectURL(gif_img); gif_img = undefined; } // 入力チェック var err; // 開始位置 var fr_h, fr_m, fr_s, fr_ms; err = true; fr_h = elem_h[0].value.trim(); fr_h = (fr_h == "") ? 0 : (fr_h.search(/^\d+$/) == -1) ? -1 : parseInt(fr_h, 10); if(fr_h >= 0) { fr_m = elem_m[0].value.trim(); fr_m = (fr_m == "") ? 0 : (fr_m.search(/^\d+$/) == -1) ? -1 : parseInt(fr_m, 10); if(fr_m >= 0 && fr_m < 60) { fr_s = elem_s[0].value.trim(); fr_s = (fr_s == "") ? 0 : (fr_s.search(/^\d+$/) == -1) ? -1 : parseInt(fr_s, 10); if(fr_s >= 0 && fr_s < 60) { fr_ms = elem_ms[0].value.trim(); fr_ms = (fr_ms == "") ? 0 : (fr_ms.search(/^\d+$/) == -1) ? -1 : parseInt(fr_ms, 10); if(fr_ms >= 0 && fr_ms < 1000) err = false; } } } if(err) { alert("開始位置の入力に誤りがあります。"); return; } // 終了位置 var to_h, to_m, to_s, to_ms; err = true; to_h = elem_h[1].value.trim(); to_m = elem_m[1].value.trim(); to_s = elem_s[1].value.trim(); to_ms = elem_ms[1].value.trim(); if(to_h == "" && to_m == "" && to_s == "" && to_ms == "") { // 入力なし to_time = Math.floor(elem_vid.duration * 1000); } else { to_time = undefined; to_h = (to_h == "") ? 0 : (to_h.search(/^\d+$/) == -1) ? -1 : parseInt(to_h, 10); if(to_h >= 0) { to_m = (to_m == "") ? 0 : (to_m.search(/^\d+$/) == -1) ? -1 : parseInt(to_m, 10); if(to_m >= 0 && to_m < 60) { to_s = (to_s == "") ? 0 : (to_s.search(/^\d+$/) == -1) ? -1 : parseInt(to_s, 10); if(to_s >= 0 && to_s < 60) { to_ms = (to_ms == "") ? 0 : (to_ms.search(/^\d+$/) == -1) ? -1 : parseInt(to_ms, 10); if(to_ms >= 0 && to_ms < 1000) err = false; } } } if(err) { alert("終了位置の入力に誤りがあります。"); return; } } fr_time = (fr_h * 3600 + fr_m * 60 + fr_s) * 1000 + fr_ms; if(fr_time >= elem_vid.duration * 1000) { alert("開始位置が動画の長さを超えています。"); return; } if(to_time == undefined) { // 終了位置 入力あり to_time = (to_h * 3600 + to_m * 60 + to_s) * 1000 + to_ms; if(to_time > elem_vid.duration * 1000) { alert("終了位置が動画の長さを超えています。"); return; } if(fr_time > to_time) { alert("終了位置が開始位置より前です。"); return; } } // フレーム間隔 interval = elem_int.value; interval = (interval.search(/^\s*\d+\s*$/) == -1) ? 0 : parseInt(interval, 10); if(interval <= 0 || interval > 65535) { alert("フレーム間隔の入力に誤りがあります。"); return; } // ループ回数 if(elem_lp_cnt.checked) { lp_times = elem_lp_times.value; lp_times = (lp_times.search(/^\s*\d+\s*$/) == -1) ? 0 : parseInt(lp_times, 10); if(lp_times <= 1 || lp_times > 65536) { alert("ループ再生の再生回数の入力に誤りがあります。"); return; } } elem_vid.controls = false; elem_vid.pause() save_time = elem_vid.currentTime; with(elem_work) { width = elem_gif.width = img_width = elem_vid.videoWidth; height = elem_gif.height = img_height = elem_vid.videoHeight; ctx_work = getContext("2d"); } data_len = (img_width * img_height) << 2; // GIF データ gif_data = []; gif_block = new Uint8Array(10000); i_gif_block = 0; gif_header[I_SCREEN_WIDTH ] = gif_frame[I_IMAGE_WIDTH ] = img_width & 0xff; gif_header[I_SCREEN_WIDTH + 1] = gif_frame[I_IMAGE_WIDTH + 1] = img_width >> 8; gif_header[I_SCREEN_HEIGHT ] = gif_frame[I_IMAGE_HEIGHT ] = img_height & 0xff; gif_header[I_SCREEN_HEIGHT + 1] = gif_frame[I_IMAGE_HEIGHT + 1] = img_height >> 8; gif_frame[I_DELAY_TIME ] = interval & 0xff; gif_frame[I_DELAY_TIME + 1] = interval >> 8; // Header,Logical Screen Descriptor for(i = 0; i < gif_header.length; i++) putc(gif_header[i]); // ループ再生 if(!elem_lp_never.checked) { // ループ再生 あり // Application Extension (Loop) if(elem_lp_cnt.checked) { // 回数指定 var times = lp_times - 1; gif_loop[I_LOOP_TIMES ] = times & 0xff; gif_loop[I_LOOP_TIMES + 1] = times >> 8; } else { // 無限 gif_loop[I_LOOP_TIMES] = gif_loop[I_LOOP_TIMES + 1] = 0; } for(i = 0; i < gif_loop.length; i++) putc(gif_loop[i]); } frames = 0; cancel = false; elem_cre.value = "中止"; elem_sts.textContent = "作成中"; // 最初のフレーム elem_vid.currentTime = fr_time / 1000; } function vid_seeked() { if(frames == undefined) // 作成中でない return; if(cancel) { // 中止 frames = undefined; elem_file.disabled = false; elem_vid.currentTime = save_time; elem_vid.controls = true; elem_cre.value = "作成"; elem_sts.textContent = elem_frms.textContent = elem_time.textContent = ""; return; } elem_frms.textContent = String(++frames); var gif_time = frames * interval; var time = fr_time + gif_time * 10; if(time > to_time) { var excess = Math.floor((time - to_time) / 10); gif_time -= excess; var delay = interval - excess; gif_frame[I_DELAY_TIME ] = delay & 0xff; gif_frame[I_DELAY_TIME + 1] = delay >> 8; } var cs = gif_time % 100; var int = (gif_time - cs) / 100; var sec = int % 60; var min = (int - sec) / 60; elem_time.textContent = ((min) ? String(min) + "分 " : "") + String(sec) + "秒 " + String(cs * 10) + "ms"; // Firefox では,幅/高さを指定しないと正しく描画されない場合がある. //ctx_work.drawImage(elem_vid, 0, 0); ctx_work.drawImage(elem_vid, 0, 0, img_width, img_height); create_frame(ctx_work.getImageData(0, 0, img_width, img_height).data); // フレーム作成 if(time < to_time) { // 次のフレーム elem_vid.currentTime = time / 1000; return; } putc(0x3b); // Trailer // GIF データ フラッシュ if(i_gif_block) gif_data.push(new Uint8Array(gif_block.buffer, 0, i_gif_block)); gif_img = URL.createObjectURL(new Blob(gif_data, {type:"image/gif"})); gif_page = URL.createObjectURL(new Blob( ["<HTML><BODY STYLE='margin:0'><IMG SRC='" + gif_img + "'></BODY></HTML>"], {type:"text/html;charset=US-ASCII"})); new_img = true; frames = undefined; elem_file.disabled = false; elem_vid.currentTime = save_time; elem_vid.controls = true; elem_cre.value = "作成"; elem_sts.textContent = "作成完了"; elem_view.disabled = false; elem_save.disabled = false; } // フレーム作成 function create_frame(data) { reduce_color(data); // 256色に減色 var prop; var i; // Graphic Control Extension,Image Descriptor for(i = 0; i < gif_frame.length; i++) putc(gif_frame[i]); // Local Color Table for(i = 0; i < n_colors; i++) { putc(colors_r[i]); putc(colors_g[i]); putc(colors_b[i]); } // ダミー for(i = (256 - i) * 3; i; i--) putc(0); // LZW Minimum Code Size putc(8); // 8 ビット // 辞書ルート // 機能的には Map が適しているが,こちらの方が速かった. var root_prop = []; while(root_prop.length < 256) { (prop = []).chara = emp_str; root_prop.push(prop); } var n_code = 258; // コード登録数 var code_bits = 9; // コード ビット数 var max_n = 512; // 最大コード登録数 var dict_code; // コード var dict_prop; // 辞書情報 var pack_buff = 0; // コード パック用バッファ var pack_bits = 0; // コード パック用バッファ格納ビット数 var block_cnt = 0; // ブロック バッファ格納バイト数 put_code(256); // Clear for(var i_data = 0; i_data < data_len; i_data +=4) { // 近似色のインデックスを求める var r = data[i_data ]; var g = data[i_data + 1]; var b = data[i_data + 2]; var min = 0x7fffffff; var c; for(i = 0; i < n_colors; i++) { var dr = r - colors_r[i]; var dg = g - colors_g[i]; var db = b - colors_b[i]; var ra = (r << 1) - dr; // r + colors_r[i] var d2 = ((1024 + ra) * (dr * dr)) + ((dg * dg) << 11) + ((1536 - ra) * (db * db)); if(d2 < min) { min = d2; c = i; } } if(!i_data) { // 最初のピクセル dict_prop = root_prop[dict_code = c]; continue; } var cs = String.fromCharCode(c); // 辞書サーチ if((i = dict_prop.chara.indexOf(cs)) < 0) { // 未登録 put_code(dict_code); if(n_code == 4096) { // 辞書が一杯 // 辞書クリア put_code(256); // Clear for(i = 0; i < 256; i++) { prop = root_prop[i]; prop.chara = emp_str; prop.length = 0; } n_code = 258; code_bits = 9; max_n = 512; } else { // 辞書に登録 dict_prop.chara += cs; (prop = []).chara = emp_str; prop.code = n_code; dict_prop.push(prop); if(n_code++ == max_n) { code_bits++; max_n <<= 1; } } dict_prop = root_prop[dict_code = c]; } else { // 登録済み dict_code = (dict_prop = dict_prop[i]).code; } } put_code(dict_code); // 最後のコード put_code(257); // End of Information // コード パック用バッファ フラッシュ if(pack_bits) put_code(0); // ブロック バッファ フラッシュ if(block_cnt) { putc(block_cnt); for(i = 0; i < block_cnt; i++) putc(block_buff[i]); } putc(0x00); // Block Terminator //---------------- // LZW コード出力 function put_code(code) { pack_buff |= code << pack_bits; pack_bits += code_bits; while(pack_bits >= 8) { block_buff[block_cnt++] = pack_buff & 0xff; if(block_cnt == 255) { putc(255); for(var i = 0; i < 255; i++) putc(block_buff[i]); block_cnt = 0; } pack_buff >>= 8; pack_bits -= 8; } } } // 256色に減色 function reduce_color(data) { var leaves = 0; // 葉の数 var root = new Map(); // ルート ノード var internal = {}; // 内部ノード リスト for(var i_data = 0; i_data < data_len; i_data += 4) { var r = data[i_data ]; var g = data[i_data + 1]; var b = data[i_data + 2]; var color = (r << 16) | (g << 8) | b; // 色追加 for(var level = 0, node = root; ; level++, node = child) { var key = color & (0x808080 >> level); var child = node.get(key); if(child == undefined) { // ノードなし // ノード追加 node.set(key, child = new Map()); if(level == 7) { // 葉 child.pixels = 1; child.r_sum = r; child.g_sum = g; child.b_sum = b; leaves++; while(leaves > 256) { // 葉の数が 256 より多い // 葉を減らす for(level = 6; ; level--) { if(internal[level] != undefined) break; } node = internal[level]; internal[level] = node.next; var pixels = 0; var r_sum = 0; var g_sum = 0; var b_sum = 0; for(child of node.values()) { pixels += child.pixels; r_sum += child.r_sum; g_sum += child.g_sum; b_sum += child.b_sum; } leaves -= node.size - 1; node.clear(); // 子ノード リスト クリア node.pixels = pixels; node.r_sum = r_sum; node.g_sum = g_sum; node.b_sum = b_sum; } break; } else { child.next = internal[level]; internal[level] = child; } } else { if(!child.size) { // 葉 child.pixels++; child.r_sum += r; child.g_sum += g; child.b_sum += b; break; } } } } // カラー テーブル作成 var i_color = 0; get_colors(root); n_colors = leaves; //---------------- // カラー テーブル作成 function get_colors(node) { for(var child of node.values()) { if(child.size) { // 内部ノード get_colors(child); } else { colors_r[i_color] = child.r_sum / child.pixels; colors_g[i_color] = child.g_sum / child.pixels; colors_b[i_color] = child.b_sum / child.pixels; i_color++; } if(i_color == leaves) break; } } } // GIF データに 1 バイト出力 function putc(c) { gif_block[i_gif_block++] = c; if(i_gif_block == 10000) { gif_data.push(gif_block); gif_block = new Uint8Array(10000); i_gif_block = 0; } } // GIF 表示 function view_gif() { elem_vid.pause() if(new_img) { elem_gif.src = gif_page; new_img = false; } else { elem_gif.contentDocument.location.reload(); } elem_gif.style.visibility = ""; elem_cls.disabled = false; } // GIF 表示終了 function close_gif() { elem_gif.style.visibility = "hidden"; elem_cls.disabled = true; } // GIF ファイル保存 function save_gif() { with(document.getElementById("dl")) { var i = file_name.lastIndexOf("."); download = ((i <= 0) ? file_name : file_name.substring(0, i)) + ".gif"; if(href != "") URL.revokeObjectURL(href); href = URL.createObjectURL(new Blob(gif_data, {type:"application/octet-stream"})); click(); } } // 整数 キー入力チェック function check_int(e) { return (e.which < 0x20 || e.which >= 0x30 && e.which <= 0x39 || e.ctrlKey || e.metaKey); } //---------------------------------------------------------- // カラー テーブル colors_r = new Uint8Array(256); colors_g = new Uint8Array(256); colors_b = new Uint8Array(256); block_buff = new Uint8Array(255); // ブロック バッファ // Header,Logical Screen Descriptor gif_header = [ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // Signature,Version GIF89a 0x00, 0x00, 0x00, 0x00, // Logical Screen Width,Logical Screen Height 0x77, // Global Color Table Flag,Color Resolution, // Sort Flag,Size of Global Color Table // 8 ビット,256色 0x00, // Background Color Index 0x00 // Pixel Aspect Ratio ]; I_SCREEN_WIDTH = 6; I_SCREEN_HEIGHT = 8; // Application Extension (Loop) gif_loop = [ 0x21, // Extension Introducer 0xff, // Application Label 0x0b, // Block Size 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, // Application Identifier NETSCAPE 0x50, 0x45, 0x32, 0x2e, 0x30, // Appl. Authentication Code 2.0 0x03, // Sub-block Data Size 0x01, // Sub-block ID 0x00, 0x00, // Number of Times 0 // Block Terminator ]; I_LOOP_TIMES = 16 // Graphic Control Extension,Image Descriptor gif_frame = [ 0x21, // Extension Introducer 0xf9, // Graphic Control Label 4, // Block Size 0x04, // Disposal Method,User Input Flag,Transparent Color Flag // Do not dispose 0x00, 0x00, // Delay Time 0, // Transparent Color Index 0, // Block Terminator 0x2c, // Image Separator 0x00, 0x00, 0x00, 0x00, // Image Left Position,Image Top Position 0x00, 0x00, 0x00, 0x00, // Image Width,Image Height 0x87, // Local Color Table Flag,Interlace Flag, // Sort Flag,Size of Local Color Table // Local Color Table,256色 ]; I_DELAY_TIME = 4; I_IMAGE_WIDTH = 13; I_IMAGE_HEIGHT = 15; elem_file = document.getElementById("file"); elem_vid = document.getElementById("vid"); elem_gif = document.getElementById("gif"); elem_h = [document.getElementById("fr_h"), document.getElementById("to_h")]; elem_m = [document.getElementById("fr_m"), document.getElementById("to_m")]; elem_s = [document.getElementById("fr_s"), document.getElementById("to_s")]; elem_ms = [document.getElementById("fr_ms"), document.getElementById("to_ms")]; elem_fr_cur = document.getElementById("fr_cur"); elem_to_cur = document.getElementById("to_cur"); elem_int = document.getElementById("int"); elem_lp_never = document.getElementById("lp_never"); elem_lp_cnt = document.getElementById("lp_cnt"); elem_lp_times = document.getElementById("lp_times"); elem_cre = document.getElementById("cre"); elem_sts = document.getElementById("sts"); elem_frms = document.getElementById("frms"); elem_time = document.getElementById("time"); elem_view = document.getElementById("view"); elem_cls = document.getElementById("cls"); elem_save = document.getElementById("save"); elem_work = document.createElement("CANVAS"); elem_vid.addEventListener("canplay", vid_canplay, false); elem_vid.addEventListener("error", vid_error, false); elem_vid.addEventListener("seeked", vid_seeked, false); elem_h[0].onkeypress = elem_m[0].onkeypress = elem_s[0].onkeypress = elem_ms[0].onkeypress = elem_h[1].onkeypress = elem_m[1].onkeypress = elem_s[1].onkeypress = elem_ms[1].onkeypress = check_int; elem_int.onkeypress = check_int; elem_lp_times.onkeypress = check_int; elem_lp_times.onclick = lp_times_click; frames = undefined; gif_img = undefined new_img = false; emp_str = ""; // ページを再ロードしたときのため document.forms[0].reset(); elem_file.disabled = false; elem_fr_cur.disabled = elem_to_cur.disabled = true; elem_cre.disabled = true; elem_view.disabled = true; elem_cls.disabled = true; elem_save.disabled = true; //--> </SCRIPT> </BODY> </HTML> |