Quantcast
Channel: CSSタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8700

【Vue】漫画の進捗管理ツール作ってみた

$
0
0

01.png
02.png
03.png

あと何コマ?
コマ数で漫画の進捗管理するツールを作りました。
コマ数を入力するとコマが出てきて、
終わったコマをクリックすると塗りつぶされて完了状態になります。
1コマ辺りの作業時間を入力すると、残りの作業時間がわかります。
1コマの作業時間×残りコマ数で残りの作業時間が算出される訳ですね。

何故作ろうと思ったのか

ページ単位で管理するツールは既にあるが、
コマ単位で管理するものはなかったため。
漫画制作のモチベーションを維持するためにこういうツールがほしかった。
(コマ単位で管理しないとモチベが保たない)
Vue初心者が悶絶しながら作ったものですが、この記事が他の勉強中の方の参考になればと思います。

ソース全文

panels.html
<!DOCTYPE html><htmllang="ja"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width, initial-scale=1"><title>あと何コマ?</title><!-- Bootstrap --><linkrel="stylesheet"href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"crossorigin="anonymous"><!-- fontawesome --><linkrel="stylesheet"href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.9.0/css/all.min.css"integrity="sha256-UzFD2WYH2U1dQpKDjjZK72VtPeWP50NoJjd26rnAdUI="crossorigin="anonymous"/><linkrel="stylesheet"href="panels.css"></head><body><headerclass="d-flex flex-column flex-md-row align-items-center p-3 px-md-4"><navclass="mt-2 my-md-0 mr-md-3"><aclass="px-2 text-white"href="#paneldiv">あと何コマ?</a><aclass="px-2 text-white"href="#timediv">あと何分?</a></nav></header><divclass="jumbotron jumbotron-fluid"><divclass="container captionText"><p>コマ数で進捗管理するツールです。残りコマ数を入力して、入力完了ボタンを押してください。<br>
        1コマ辺りの作業時間を入力すると、残りの作業時間がわかります。
      </p></div></div><divid="app"class="mb-5"><divclass="container main py-4 mt-sm-3"><articleclass="text-center pt-3 pb-4"id="paneldiv"><divclass="alertArea text-center mb-3"><strongv-show="alertShow">コマ数を入力してください
          </strong></div><h3v-if="!resultShow">あと<inputtype="number"v-model.number="remainedPanelsInput"min="0"class="panelInput">コマ?
          <buttontype="button"class="ml-2 btn page-link text-light d-inline-block btn-purple"@click="resultShowFunc"v-if="!resultShow">入力完了</button></h3><h3v-else><inputtype="number"v-model.number="remainedPanelsInput"min="0"class="panelInput">コマ
          <buttontype="button"class="ml-2 btn page-link text-light d-inline-block btn-purple"@click="resultReset"v-if="resultShow">リセット</button></h3><divv-show="resultShow">全{{ remainedPanelsInput }}コマ-
          済み<inputtype="number"v-model.number="filledPanels"v-bind:max="remainedPanelsInput"min="0">コマ
          =
          あと<spanclass="resultText">{{ remainedPanelsNumber }}</span>コマ
          <pclass="text-muted pt-3">終わったコマをクリックすると、塗りつぶされて完了状態になります。完了状態のコマをクリックすると未完の状態に戻ります。</p></div><sectionclass="row pricing-header px-3 py-3 pt-md-3 pb-md-1 mx-auto text-center"v-show="resultShow"><divclass="panel"v-for="n in remainedPanelsNumber"@click="panelFinished"><divclass="panelInner">{{ n }}</div></div><divclass="panel filled"v-for="n in filledPanels"@click="panelReturn"></div></section></article><articleclass="text-center"id="timediv"v-show="resultShow"><h3>あと何分?</h3><div>
          1コマ辺りの作業時間<inputclass="inputPerPanel"type="number"v-model.number="
          perPanel"min="0">分×残り{{ remainedPanelsNumber }}コマ=
          あと<spanclass="resultText">{{ remainingTime }}</span>分
          ({{ remainingHour }}時間)
        </div></article></div><!-- ツイートボタン --><divclass="contact text-center"><ahref="https://twitter.com/share"class="twitter-share-button"data-url="https://mitaru.github.io/panels/"data-text="進捗どうですか?あと何コマ?"data-size="large"data-hashtags="あと何コマ">
        Tweet
      </a></div></div><footerclass="my-1 pt-5 text-muted text-center text-small"><ulclass="list-inline"><liclass="list-inline-item"><ahref="https://twitter.com/SakaiMitaru"><iclass="fab fa-twitter-square mr-1"></i>SakaiMitaru
        </a></li><liclass="list-inline-item"><ahref="https://github.com/mitaru/panels.git"><iclass="fab fa-github"></i></a></li></ul></footer><script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj"crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js"integrity="sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd"crossorigin="anonymous"></script><!-- vue.js --><script src="https://cdn.jsdelivr.net/npm/vue"></script><script src="panels.js"></script></body></html>
panels.css
html{font-size:14px;}@media(min-width:768px){html{font-size:16px;}}body{color:rgb(31,45,65);background:#F7F7FC;}header{background:#37384E;color:#fff;}.jumbotron{margin:0;background:#37384E;color:#fff;border-radius:0040%40%;}.captionTextp{margin-bottom:50px;}.text-muted{color:#9e9fb4!important;font-size:14px;margin:0;}.main{max-width:2000px;background:#F7F7FC;}.alertArea{height:20px;}.alertAreastrong{background:rgba(255,255,255,0.6)!important;padding:2px5px;border-radius:4px;color:#37384E;}.panelInput{min-width:10vw;}.btn-purple{color:#fff;background:#16C995;border:0;}.btn-purple:hover{background:#0f926d;}.pricing-header{max-width:1000px;}section{justify-content:center;align-items:center;}input{width:50px;border:0;background:#fff;border-radius:4px;padding:3px;margin:2px;color:#766DF4;font-weight:bold;text-align:center;}.resultText{background:rgba(118,109,244,0.08)!important;color:#766df4!important;font-size:20px;padding:010px;border-radius:4px;font-weight:bold;}.panel{width:150px;height:100px;background:#fff;border:5pxsolid#333;margin:4px;text-align:center;line-height:100px;cursor:pointer;user-select:none}.filled{background-color:#fff;background-image:radial-gradient(#16C99514%,transparent17%),radial-gradient(#16C99514%,transparent17%);background-position:00,4px4px;background-size:8px8px;}footer{background:#F7F7FC;clear:both;}a.fa-github{font-size:30px;color:#333;}a.fa-github:hover{opacity:0.8;}@mediascreenand(max-width:480px){.panel{width:100px;height:70px;line-height:70px;}}
panels.js
(function(){'use strict';newVue({el:'#app',data:{remainedPanelsInput:0,filledPanels:0,perPanel:0,resultShow:false,alertShow:false,},watch:{remainedPanelsInput:{handler:function(){localStorage.setItem('remainedPanelsInput',JSON.stringify(this.remainedPanelsInput));},deep:true},filledPanels:{handler:function(){localStorage.setItem('filledPanels',JSON.stringify(this.filledPanels));},deep:true},perPanel:{handler:function(){localStorage.setItem('perPanel',JSON.stringify(this.perPanel));},deep:true},},methods:{resultShowFunc:function(){if(this.remainedPanelsInput===0){this.alertShow=true;}else{this.resultShow=true;this.alertShow=false;}},panelFinished:function(){this.filledPanels++;},panelReturn:function(){this.filledPanels--;},resultReset:function(){this.remainedPanelsInput=0;this.filledPanels=0;this.perPanel=0;this.resultShow=false;},},computed:{remainedPanelsNumber:function(){returnthis.remainedPanelsInput-this.filledPanels;},remainingTime:function(){returnthis.remainedPanelsNumber*this.perPanel;},remainingHour:function(){returnMath.round((this.remainingTime/60)*10)/10;},},mounted:function(){this.remainedPanelsInput=JSON.parse(localStorage.getItem('remainedPanelsInput'))||0;this.filledPanels=JSON.parse(localStorage.getItem('filledPanels'))||0;this.perPanel=JSON.parse(localStorage.getItem('perPanel'))||0;if(this.remainedPanelsInput>0){this.resultShow=true}},})// twitter投稿!function(d,s,id){varjs,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document,'script','twitter-wjs');})();

Bootstrap4のこちらの実例をもとに作りました。ほぼ原型残ってないです。
何故わざわざテンプレートをもとに作るかと言うと、レスポンシブ対応が楽だからですね。
10.png
全力で先人に頼っていくスタイル。

Vue部分

panels.js
newVue({el:'#app',data:{remainedPanelsInput:0,filledPanels:0,perPanel:0,resultShow:false,alertShow:false,},// 中略computed:{remainedPanelsNumber:function(){returnthis.remainedPanelsInput-this.filledPanels;},remainingTime:function(){returnthis.remainedPanelsNumber*this.perPanel;},remainingHour:function(){returnMath.round((this.remainingTime/60)*10)/10;},},

02.png
remainedPanelsInput(全○コマの数)から filledPanels(完了したコマ数)を引いて
remainedPanelsNumber(残りコマ数)を出しています。
あと何分?の部分はperPanel(1コマ辺りの作業時間)と
remainedPanelsNumber(残りコマ数)を掛けて算出しています。
また分単位だけでなく時間単位の表記もあった方が親切だと思ったので
remainingHourで計算しました。
Math.round((this.remainingTime / 60) * 10) / 10;と書くことで、
小数点第二位で切り捨てて表示することができます。

panels.html
<h3v-if="!resultShow">あと<inputtype="number"v-model.number="remainedPanelsInput"min="0"class="panelInput">コマ?
  <buttontype="button"class="ml-2 btn page-link text-light d-inline-block btn-purple"@click="resultShowFunc"v-if="!resultShow">入力完了</button></h3><h3v-else><inputtype="number"v-model.number="remainedPanelsInput"min="0"class="panelInput">コマ
  <buttontype="button"class="ml-2 btn page-link text-light d-inline-block btn-purple"@click="resultReset"v-if="resultShow">リセット</button></h3>
panels.js
methods:{resultShowFunc:function(){if(this.remainedPanelsInput===0){this.alertShow=true;}else{this.resultShow=true;this.alertShow=false;}},

こちらはあと何コマ?部分のコードです。
image.png
image.png
if (this.remainedPanelsInput === 0)
あと何コマ?の入力欄が0の場合、入力完了ボタンを押下コマ数を入力してください」とアラートが表示されます。
image.png
1以上の数字が入力されている場合は結果が表示されます。
このような表示の分岐にv-showv-ifは大変便利です。

panels.html
<divclass="panel"v-for="n in remainedPanelsNumber"@click="panelFinished"><divclass="panelInner">{{ n }}</div></div><divclass="panel filled"v-for="n in filledPanels"@click="panelReturn"></div>
panels.js
methods:{// 中略panelFinished:function(){this.filledPanels++;},panelReturn:function(){this.filledPanels--;},

image.png
image.png
こちらはコマ部分です。
白いコマはremainedPanelsNumber(残りコマ数)分、
ドットのコマはfilledPanels(完了したコマ数)分表示されます。
v-for="n in remainedPanelsNumber"と書けば
remainedPanelsNumberの数だけコマを複製してくれます。楽ちんです。
jsで作ろうとしたらコマの中にコマ数を表示するのも大変そうですが、
Vueなら{{ n }}と書くだけですみます。おお助かる助かる。
またpanelFinishedpanelReturnのクリックイベントで
パネルの完了状態を変更しています。

panels.js
watch:{remainedPanelsInput:{handler:function(){localStorage.setItem('remainedPanelsInput',JSON.stringify(this.remainedPanelsInput));},deep:true},filledPanels:{handler:function(){localStorage.setItem('filledPanels',JSON.stringify(this.filledPanels));},deep:true},perPanel:{handler:function(){localStorage.setItem('perPanel',JSON.stringify(this.perPanel));},deep:true},},// 中略mounted:function(){this.remainedPanelsInput=JSON.parse(localStorage.getItem('remainedPanelsInput'))||0;this.filledPanels=JSON.parse(localStorage.getItem('filledPanels'))||0;this.perPanel=JSON.parse(localStorage.getItem('perPanel'))||0;if(this.remainedPanelsInput>0){this.resultShow=true}},

ローカルストレージでremainedPanelsInputfilledPanels
perPanelの数を保存しています。
監視している変数の変更をトリガーにして勝手に働いてくれる
監視プロパティくんは便利やでホンマ。
途中保存がうまく行かなかったんですが、
mounted部分をnew Vue内で一番最後に配置したら
ちゃんと保存されるようになりました。どうして?(無知)

デザイン部分

Bootstrap5のとあるテーマを大いに参考にさせていただきました。
見た目もできるだけ可愛くしたかったんです。
image.png

panels.css
.jumbotron{margin:0;background:#37384E;color:#fff;border-radius:0040%40%;}

border-radiusだけでdivの下部分を丸くできるものなんですね。
今回調べて初めて知りました。他にも色んな表現ができるみたいです。
歪んだ円まで作れるなんてすごい!
参考:今さら聞けない!? CSSのborder-radiusで様々な角丸に挑戦!

image.png

panels.css
.filled{background-color:#fff;background-image:radial-gradient(#16C99514%,transparent17%),radial-gradient(#16C99514%,transparent17%);background-position:00,4px4px;background-size:8px8px;}

なんとradial-gradientを使えば、CSSだけで漫画のトーンみたいなドットの背景が作れます。
CSSでドット柄(水玉模様)を作成 - ホームページのパーツ作成で好きなドットを作ろう!
他にもradial-gradientでストライプやチェック柄まで作れるみたいです。実際スゴイ!
参考:CSSグラデーションで作った背景パターンのサンプル

感想

  • 比較的思った通りに作れた
  • 見た目もいい感じになったと思う

課題・問題点

  • ページ単位の管理機能も作りたかったがややこしすぎてヤコになった
  • 今までの知識の延長線上だなと思うのでもっとレベルアップしたい
  • BootstrapVue使おうとして挫折した

ここでも全文載せてますが、GitHubでもコードを公開しております。
アドバイスいただけたら嬉しいです。
https://github.com/mitaru/panels

次は何を作ろうかなー。ちょっとネタ切れしてきました。


Viewing all articles
Browse latest Browse all 8700

Latest Images

Trending Articles

<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>