
先日、とあるブラウザゲーム開発で、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
}
})()
Demo
問題の回避策の解説
animate機能ではなく、setTimeoutでduration時間後にcallbackする仕様に変更しました。
あと、ちょっとした技ですが、wipe_animという関数を書いているのは、setPropertyで、transitionを同じ関数内で記述しても、正常にtransitionが上書きされてしまうだけなので、
ソコもsetTimeoutを0秒で実行するようにしています。
今後ESでも対応してくれるように願いましょう。