JavaScript
HTML5
webfont
iMovie
circliful

世のお父さんのために子供のスポーツ動画にクロマキー合成するタイマーを作りました


概要

スポーツシーズンですが、世のお父さん方は、子供達が活躍する所をせっせとビデオ撮影し、寝る間も惜しんで編集作業に勤しんでいることと思います。同情します。

そんなお父さんの動画クオリティーをアップさせるタイマーを用意しました。

https://www.exabugs.com/timer/index.html

スクリーンショット 2018-09-30 11.08.33.png

これでカッコいい動画を作成すれば、嫁や子供達から「パパ素敵!」と言われること間違いなし!やったね!

なお、本記事では iMovie と QuickTime を使用します


利用方法


タイマー使い方


  • ブラウザで上記URLにアクセス。(Chrome/Safariは動作確認済み)

  • 「リセット」でタイマーが出現し、「スタート」で開始します。

  • 設定はローカルに保存されますが、「クリア」を押すと初期化されます。

  • 緑色背景はアスペクト比 16:9 です。

  • 設定項目(左から)


    • 試合時間を「分」単位で指定

    • cd : カウントダウンかどうか。ONの場合は試合時間をカウントダウンします。

    • abs : 円形プログレスバーの一周(360度)を60分とする(サッカー方式?)か、試合時間とするか。

    • タイマーの大きさを画面幅との比率で指定

    • プログレスバーの色を指定。RGB値かred,blueのようなウェブカラー名。

      WEB色見本 https://www.colordic.org/

    • プログレスバーの前景の幅を指定

    • プログレスバーの背景の幅を指定

    • 背景色を指定。クロマキー用途には緑か青。LT大会で使うには白がいいでしょう。

    • タイマーのフォント。

      Googleウェブフォントを使用しています。

    • タイマーのフォントサイズ

    • タイマーの水平位置

    • タイマーの垂直位置

    • 緑色部分の幅と高さ。アスペクト比16:9になっています。




タイマー動画作成


  • Mac の場合は QuickTime Player を起動し、ファイル - 新規画面収録 でタイマー動画を収録します。

  • 収録開始すると以下が表示されるので、緑色部分に収録範囲を合わせて下さい。
    スクリーンショット 2018-09-30 12.15.31.png


動画編集


1. タイマーの合成


  • iMovie を起動して、スポーツ動画とタイマー動画の両方をマイメディアに取り込みます

  • 「グリーン/ブルースクリーン」を選択すると、クロマキー合成されます。

スクリーンショット_2018-09-30_12_29_24.jpg


  • タイムや怪我人の救護等で時計の進行が止まる場合は、iMovieでは「フリーズフレーム」でタイマー動画を止めるとよいでしょう。

  • ここまでで一旦、合成された動画をエクスポートします。


2. ハイライトシーンの編集


  • 1.の動画を開いて、ハイライトシーンを編集します。


プログラム


  • 一応コードも貼っておきます。

  • 簡単のため一つのHTMLに内包しています。

  • 肝心の円形プログレスバーの部分はcirclifulを使わせて頂きました。

    https://github.com/pguso/jquery-plugin-circliful

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Timer</title>
<link href="css/jquery.circliful.css" rel="stylesheet" type="text/css" />
<link href='https://fonts.googleapis.com/css?family=Roboto:900,400' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Rajdhani:900,400' rel='stylesheet' type='text/css'>
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script src="js/jquery.circliful.min.js"></script>
<style type="text/css">
body {
margin: 0;
font-size: small;
}

div {
margin: 3px;
}

button, input, select {
font-size: 100%;
}
</style>
</head>
<body>

<div id="container">
<div id="header">
<button id="RESET">リセット</button>
<button id="START">スタート</button>

<input id="total" size="2">
<input id="countdown" type="checkbox">cd
<input id="abs" type="checkbox">abs
<input id="size" size="2">%
<input id="color" size="9">
<input id="fgborder" size="2">
<input id="bgborder" size="2">

<select id="bg">
<option value="#ffffff"></option>
<option value="#00ff00"></option>
<option value="#0000ff"></option>
<option value="#000000"></option>
</select>

<select id="font_family">
<option value="Roboto" selected>Roboto</option>
<option value="Rajdhani">Rajdhani</option>
</select>
<input id="font_size" size="3">px

<select id="horizontal">
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
</select>

<select id="vertical">
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
</select>

<span id="divsize"></span>
<button id="CLEAR">クリア</button>
</div>
<div id="contents">
<div id="clock"></div>
</div>
<div id="footer">
</div>
</div>

<script>

function time(s) {
var min = Math.floor(s / 60);
var sec = Math.floor(s % 60);
return ("0" + min).slice(-2) + ":" + ("0" + sec).slice(-2);
}

function disp(prog, total, countdown) {
var _t = prog;
if (total < prog) {
_t = prog - total;
} else if (countdown) {
_t = total - prog;
}
return time(_t / 1000);
}

function percent(prog, total, abs) {
var hour = 60 * 60 * 1000;
var all = abs ? hour : total;
return Math.max((total - prog) / all, 0);
}

function shadow(color) {
return [
[2, 0], [-2, 0], [0, -2], [0, 2],
[2, 2], [-2, 2], [2, -2], [-2, -2],
[1, 2], [-1, 2], [1, -2], [-1, -2],
[2, 1], [-2, 1], [2, -1], [-2, -1]
].map(function(i) {
return color + " " + i.map(function(j) {
return j + "px"
}).join(" ")
}).join(",");
}

function conf(total, abs, countdown) {

var font_size = $('#font_size').val();
var font_family = $('#font_family').val();

var textStyle = [
["font-size", font_size + "px"],
["font-family", font_family],
["text-shadow", shadow("black")]
].map(function(i) {
return i[0] + ": " + i[1] + ";"
}).join(" ");

var prog = 0;
return {
percent: Math.floor(percent(prog, total, abs) * 100),
replacePercentageByText: '',
text: disp(prog, total, countdown),
textStyle: textStyle,
foregroundBorderWidth: $("#fgborder").val(),
backgroundBorderWidth: $("#bgborder").val(),
foregroundColor: $("#color").val(),
textColor: "white",
backgroundColor: "white",
}
}

///////////////////////////////////////////////////

var storage = localStorage;

function saveText(id) {
storage.setItem(id, $("#" + id).val());
}

function saveCheck(id) {
var val = $("#" + id).prop("checked");
storage.setItem(id, val);
}

function loadText(id, def) {
var val = storage.getItem(id);
val = (val === null ? def : val);
$("#" + id).val(val).change();
}

function loadCheck(id, def) {
var val = storage.getItem(id);
val = (val === null ? def : (val === "false" ? false : true));
$("#" + id).prop("checked", val).change();
}

function save() {
saveCheck("countdown");
saveCheck("abs");
saveText("total");
saveText("size");
saveText("color");
saveText("bg");
saveText("font_family");
saveText("font_size");
saveText("horizontal");
saveText("vertical");
saveText("fgborder");
saveText("bgborder");
}

function load() {
loadCheck("countdown", true);
loadCheck("abs", true);
loadText("total", 45);
loadText("size", 15);
loadText("color", "#3498DB");
loadText("bg", "#00ff00");
loadText("font_family", "Roboto");
loadText("font_size", 36);
loadText("horizontal", 0);
loadText("vertical", 0);
loadText("fgborder", 11);
loadText("bgborder", 15);
}

function clear() {
storage.clear();
load();
}

///////////////////////////////////////////////////

var data = {};

function stop() {
clearInterval(data.timer);
delete data.timer;
}

function reset() {
stop();
save();

data.total = Number($("#total").val()) * 60 * 1000;
data.countdown = $("#countdown").prop("checked");
data.abs = $("#abs").prop("checked");

var countdown = data.countdown;
var total = data.total;
var abs = data.abs;

$('#clock').remove();
$('#contents').append('<div id="clock"></div>');

$('#clock').circliful(conf(total, abs, countdown));

var svg = $("#clock").children("svg").eq(0);

data.svg = {
container: svg,
text: svg.children("text").eq(0),
circle: svg.children("circle").eq(1)
};

resize();
}

function start() {
stop();

data.start = Date.now();

data.timer = setInterval(function() {
var countdown = data.countdown;
var total = data.total;
var abs = data.abs;
var prog = Date.now() - data.start;

var degree = Math.floor(percent(prog, total, abs) * 360);

data.svg.text.text(disp(prog, total, countdown));
data.svg.circle.css("stroke-dasharray", degree + ", 20000");
}, 200);
}

$('#RESET').click(reset);
$('#START').click(start);
$('#CLEAR').click(clear);

///////////////////////////////////////////////////

$("#bg").change(function() {
$("#contents").css('background-color', $(this).val());
});

function resize() {

var contents = $("#contents");
contents.height(Math.floor(contents.width() / 16 * 9));

$("#divsize").text("(" + contents.width() + "x" + contents.height() + ")");

if (data.svg) {
var ratio = $("#size").val() / 100;

var size = contents.width() * ratio;
var left = (contents.width() - size) / 2 * $("#horizontal").val();
var top = (contents.height() - size) / 2 * $("#vertical").val();

var svg = data.svg.container;
svg.css('width', size);
svg.css('padding-left', left);
svg.css('padding-top', top);
}
}

$("#horizontal").change(resize);
$("#vertical").change(resize);
$(window).resize(resize);

///////////////////////////////////////////////////

load();
resize();

</script>
</body>
</html>