[Javascript + CSS] ゲームで使えるワイプ機能(animateでmask-sizeが使えない時の対処法)
先日、とあるブラウザゲーム開発で、mask-imageを使って、ワイプ処理を作ることになったのだが、色々と出来る事と出来ない事がわかったので、メモを残しておきます。
※わかりやすいように背景に市松模様をセットしています。
事前準備
今回作るワイプ機能では、丸いオブジェクトが拡大や縮小をすることで、ワイプ-インやアウトを実現する仕様です。 PNGなどで簡易に丸い画像をピクセルで作っても良いのですが、画面いっぱいに拡大する事を考えると、サイズに依存しないsvgを採用した方がいいと思い、svgでの丸素材を作っておきます。svgの丸画像
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0,0,100,100">
<circle cx="50" cy="50" r="50" />
</svg>
svg画像を、更にcssに埋め込むために、base64文字列に変換します。
svgをbase64化
url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%2C0%2C100%2C100%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20%2F%3E%3C%2Fsvg%3E')
cssでワイプ機能をコーディング
wipe.html
<link rel='stylesheet' href='wipe.css'/>
<div class='wipe'>
<img src='sample.jpg'>
</div>
wipe.css
.wipe img{
width:100%;
height:100%;
object-fit:cover;
}
.wipe{
width:100%;
height:300px;
-webkit-mask-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%2C0%2C100%2C100%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20%2F%3E%3C%2Fsvg%3E');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
animation: 2s ease-in 1s infinite alternate wipe;
}
@keyframes wipe{
from{
-webkit-mask-size: 0;
}
to{
-webkit-mask-size: 100%;
}
}
Demo
解説
1. mask-image
どうやら、-webkit-というプレフィックスを付けないとChromeやSafariでは動作しないようなので、-webkit-mask-imageで記述してます。 念の為、mask-image:***;も書いておいたほうがいいかもしれません。(将来的にプレフィックス無し仕様に変更される可能性があるため) 上記で作成した、丸画像(svg)のbase64文字列を入れています。2. mask-repeat
繰り返しパターン表示をしないので、-webkit-mask-repeat: no-repeat;をセットしてます。3. mask-position
画面のどの位置に表示するかの指定ができます。 ピクセルしていでも、%指定でも書けますが、画面中央の場合は、centerで問題ないです。 デフォルトでは、左上の0,0になります。4. animation機能
-webkit-mask-sizeを 0~100%の範囲で行き来させています。5. ワイプ適用範囲
mask-imageをセットしたタグの内包する要素全てに対して、ワイプが適用されます。Javascript(失敗パターン)
次に上記のcss表示をjavascriptでコントロールできるようにセットしてみます。wipe_js.html
<link rel='stylesheet' href='wipe_js.css'/>
<script src='wipe.js'></script>
<div class='frame'>
<div class='wipe-js'>
<img src='gdpr-3518253_1280.jpg'>
</div>
</div>
<button class='wipe-button'>wipe</button>
wipe.css
.frame{
border:1px solid black;
background: linear-gradient(45deg, #3331 25%, transparent 25%, transparent 75%, #3331 75%),
linear-gradient(45deg, #3331 25%, transparent 25%, transparent 75%, #3331 75%);
background-size: 40px 40px;
background-position: 0 0, 20px 20px;
white-space:normal;
font-size:0;
}
.frame *{
white-space:normal;
font-size:0;
}
button.wipe-button{
width:100px;
padding:10px;
border:1px solid #ccc;
background-color:#eee;
color:black;
border-radius:4px;
cursor:pointer;
margin:10px;
}
button.wipe-button:hover{
opacity:0.5;
}
button.wipe-button:active{
background-color:#F003;
}
img{
width:100%;
height:100%;
object-fit:cover;
}
.wipe-js{
width:100%;
height:300px;
-webkit-mask-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%2C0%2C100%2C100%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20%2F%3E%3C%2Fsvg%3E');
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: 0;
}
wipe.js
(function(){
function Wipe(){
this.button = this.elm_button()
this.wipe = this.elm_wipe()
if(this.button){
this.button.addEventListener('click' , this.click_wipe_button.bind(this))
}
}
Wipe.prototype.elm_button = function(){
return document.querySelector('button.wipe-button')
}
Wipe.prototype.elm_wipe = function(){
return document.querySelector('.wipe-js')
}
Wipe.prototype.click_wipe_button = function(e){
if(this.wipe.hasAttribute('data-animating')){return}
// this.wipe.style.setProperty('-webkit-mask-size' , `100%` , '')
const button = e.currentTarget
this.wipe.setAttribute('data-animating' , 1)
if(button.getAttribute('data-status') !== 'in'){
button.setAttribute('data-status' , 'in')
this.wipe_in()
}
else{
button.setAttribute('data-status' , 'out')
this.wipe_out()
}
}
Wipe.prototype.wipe_in = function(){
const time = 1000
this.wipe.style.setProperty('transition-duration' , `${time}ms` , '')
this. wipe_anim(time , '0%','120%')
}
Wipe.prototype.wipe_out = function(){
const time = 1000
this.wipe.style.setProperty('transition-duration' , `${time}ms` , '')
this. wipe_anim(time , '120%' , '0%')
}
Wipe.prototype.wipe_anim = function(time , size_from , size_to){
this.wipe.animate([
{
'-webkit-mask-size' : size_from
},
{
'-webkit-mask-size' : size_to
},
],
{
duration : time,
})
Promise.all(this.wipe.getAnimations().map(e => e.finished)).then(e=>{
console.log('finish')
this.wipe.removeAttribute('data-animating')
})
}
switch(document.readyState){
case 'complete':
case 'interactive':
new Wipe()
break
default:
window.addEventListener('load' , (()=> new Wipe()))
break
}
})()
解説
上記Javascriptコードでは、動きません。 動かない原因は、animate機能です。 animate自体は動いていて、Promiseの非同期後にちゃんとアニメーション完了のコールバックは実行されています。 keyframeの内容の、"-webkig-mask-size"がどうやら機能していないようです。 以下の設定も試してみました。-webkig-mask-size : ** WebkigMaskSize : ** mask-size : ** maskSize : **どうも出来ないことを追求しても仕方がないので、大幅に設計を変えて、animate機能を使わないバージョンを作ることにしました。
Javascript(成功パターン)
少し手直ししたバージョンです。wipe.js
(function(){
function Wipe(){
this.button = this.elm_button()
this.wipe = this.elm_wipe()
if(this.button){
this.button.addEventListener('click' , this.click_wipe_button.bind(this))
}
}
Wipe.prototype.elm_button =unction(){
return document.querySelector('button.wipe-button')
}
Wipe.prototype.elm_wipe = function(){
return document.querySelector('.wipe-js')
}
Wipe.prototype.click_wipe_button = function(e){
if(this.wipe.hasAttribute('data-animating')){return}
const button = e.currentTarget
this.wipe.setAttribute('data-animating' , 1)
if(button.getAttribute('data-status') !== 'in'){
button.setAttribute('data-status' , 'in')
this.wipe_in()
}
else{
button.setAttribute('data-status' , 'out')
this.wipe_out()
}
}
Wipe.prototype.wipe_in = function(){
const time = 1000
this.wipe.style.setProperty('-webkit-mask-size' , `0%` , '')
this.wipe.style.setProperty('transition-duration' , `${time}ms` , '')
setTimeout(this.wipe_anim.bind(this , time , '120%') , 0)
}
Wipe.prototype.wipe_out = function(){
const time = 1000
this.wipe.style.setProperty('-webkit-mask-size' , `120%` , '')
this.wipe.style.setProperty('transition-duration' , `${time}ms` , '')
setTimeout(this.wipe_anim.bind(this , time , '0%') , 0)
}
Wipe.prototype.wipe_anim = function(time , size){
this.wipe.style.setProperty('-webkit-mask-size' , size , '')
setTimeout(()=>{
this.wipe.removeAttribute('data-animating')
} , time)
}
switch(document.readyState){
case 'complete':
case 'interactive':
new Wipe()
break
default:
window.addEventListener('load' , (()=> new Wipe()))
break
}
})()