【JavaScript】 Canvas描画で特殊な合成をおこなうglobalCompositeOperation
更新日:2023/01/30
Canvas APIは画像描画時に globalCompositeOperationを使用すると、元からある画像との合成方法を指定することができます。
ここでは、globalCompositeOperationに指定できる値と結果を一覧にしてお伝えします。
デモ
まずは合成のイメージをお伝えするために、globalCompositeOperationの簡単なデモを用意しました。
画像合成設定値:
テキスト合成設定値:
参考:デモhtml
<canvas id="cvdemo" width="630" height="400" style="max-width:100%"></canvas>
画像合成設定値:<select id="cvsel1"></select>
テキスト合成設定値:<select id="cvsel2"></select>
参考:デモJavaScript
window.addEventListener('DOMContentLoaded',()=> {
const dcv = document.getElementById('cvdemo');
const context = dcv.getContext('2d');
// 選択リスト オプション内容作成
const lists = ( ( selLists , selValue ) => selLists.map(
e => {
const list = document.getElementById(e);
const fragment = document.createDocumentFragment();
selValue.forEach( e =>{
const opt = document.createElement("option");
opt.value = e;
opt.innerHTML = e;
fragment.appendChild( opt );
});
list.appendChild(fragment);
list.value = selValue[0];
list.disabled = true;
return list;
})) ( ["cvsel1","cvsel2"] , [
"source-over","source-atop","source-in","source-out","destination-over","destination-atop",
"destination-in","destination-out","lighter","copy","xor","normal","multiply","screen",
"overlay","darken","lighten","color-dodge","color-burn","hard-light","soft-light",
"difference","hue","saturation","color","luminosity"
]);
// 描画処理
const setImage = () => {
lists.forEach( e => e.disabled = false );
context.clearRect(0,0,630,400);
context.save();
context.drawImage( image[0] , 0, 0);
context.globalCompositeOperation = lists[0].value;
context.drawImage( image[1] , 0, 0);
context.globalCompositeOperation = lists[1].value;
context.font = "40px sans-serif";
context.fillStyle = "orange";
context.fillText("globalCompositeOperationデモ", 10, 200);
context.restore();
};
// イメージオブジェクト作成 & 画像読み込み待ち
const image = (( imagePath ) => {
const im = imagePath.map( e => new Image());
im.forEach( e => e.onload = () => im.every( e => e.complete ) ? setImage() : null );
im.forEach( ( e , i ) => e.src = imagePath[i] );
return im;
})( ["image01.jpg","image02.png"] );
// リスト選択イベント登録
lists.forEach( e => e.addEventListener("change", ()=>setImage() ) );
});
globalCompositeOperationの指定方法
globalCompositeOperationを使用しない場合、次のように描画した順番で上書きされます。
JavaScript
context.fillStyle = "blue";
context.fillRect( 0 , 0 , 100 , 100);
context.fillStyle = "red";
context.fillRect( 50 , 20 , 100 , 100);
context.fillStyle = "green";
context.fillRect( 70 , 40 , 100 , 100);
次のようにglobalCompositeOperationに合成方法をセットすることで、描画結果を変更することができます。
JavaScript
context.fillStyle = "blue";
context.fillRect( 0 , 0 , 100 , 100);
context.globalCompositeOperation = "destination-over";
context.fillStyle = "red";
context.fillRect( 50 , 20 , 100 , 100);
context.globalCompositeOperation = "lighter";
context.fillStyle = "green";
context.fillRect( 70 , 40 , 100 , 100);
globalCompositeOperationに指定できる値と結果
globalCompositeOperationの書式
context.globalCompositeOperation = 合成方法;
合成方法は、あらかじめ定義されている文字列で指定します。
source-XXX系
sourceはこれから描画する図形を指します。
source-で始まる合成方法は、既に描画されている図形の上に重ねる形で、新しい図形を合成処理します。
合成後、既存画像・追加画像ともに消去される可能性があります。
消去された部分は、背景色(透明)で描画されています。
ブラウザによっては、対応していないケースがあります。
『ブラウザ対応確認』ボタンを押すと、使用中のブラウザが対応しているか確認できます。
指定値 | 意味 | 結果 | ||
---|---|---|---|---|
source-XXX系 | ||||
source-over (デフォルト) | 描画した順に上書き | |||
source-atop | 重なっている部分のみ描画される | |||
source-in | 重なっている部分のみ描画される 元からある図形は消去される | |||
source-out | 重なっていない部分のみ描画される 元からある図形は消去される |
※青・緑の矩形を初期値で描画した後、globalCompositeOperationに合成方法を指定して、赤い矩形を描画しています。
destination-XXX系
destinationは、既に描画されている図形を指します。
destination-で始まる合成方法は、既に描画されている図形の下に重ねる形で、新しい図形を合成処理します。
合成後、既存画像・追加画像ともに消去される可能性があります。
消去された部分は、背景色(透明)で描画されています。
指定値 | 意味 | 結果 | ||
---|---|---|---|---|
destination-XXX系 | ||||
destination-over | 元からある図形の下に描画 | |||
destination-atop | 元からある図形の重なっていない部分が消去される | |||
destination-in | 元からある図形の重なっている部分のみ描画される | |||
destination-out | 元からある図形の重なっていない部分のみ描画される |
その他系
指定値 | 意味 | 結果 | ||
---|---|---|---|---|
その他系 | ||||
lighter | カラー値を加算して描画 例: 青00F + 赤F00 → F0F 緑008000 + 赤F00 → FF8000 | |||
copy | 元からある図形を消去して新しい図形を描画 | |||
xor | 元重なっている部分を消去 それ以外は描画 |
mix-blend-mode系
cssのmix-blend-modeで使用できる値をセットすることができます。
mix-blend-modeには、Separable blend modesとNon-separable blend modesの2種類あります。
Separable blend modes
Separable blend modesは、RGB各色毎に合成処理を行います。
指定値 | 意味 | 結果 | ||
---|---|---|---|---|
mix-blend-mode系 Separable blend modes | ||||
normal | 標準的な合成 (そのまま上書き) をおこないます | |||
multiply | カラー値を乗算します 暗い色になる傾向があります。 例: 青:00F × 赤:F00 → 黒 : 000 | |||
screen | カラー値を反転した後乗算し、 その結果を反転します 明るい色になる傾向があります。 例: 青:00F 反転→FF0 赤:F00 反転→0FF 乗算 FF0× 0FF → 0F0 反転 0F0 → F0F | |||
overlay | 既存の図形の色が明るい場合 (カラー値128以上)はscreenで明るく、 暗い場合はmultiplyで暗く描画されます。 ※既存の図形の色を 暗いときは明るく、 明るいときは暗くしてから計算するため、 screenまたは multiply指定時と同じ結果になりません。 | |||
darken | RGB各色で暗い色 (カラー値が小さい)が選択されます | |||
lighten | RGB各色で明るい色 (カラー値が大きい)が選択されます | |||
color-dodge | 既存の図形の色を、 追加する図形の色を反転させた色で 割った値で描画します。 例: 赤:F00 反転→0FF 青:00F ÷ 0FF → 001(00F) 緑:008000 ÷ 00FFFF → 008000 | |||
color-burn | 既存の図形の色を反転させ、 追加する図形の色で割り、 さらに反転させた値で描画します。 例: 青:00F 反転→FF0 FF0 ÷ 赤:F00 → 110(FF0) (分母0のとき結果を1とする) FF0 反転→00F | |||
hard-light | 追加するの図形の色が明るい場合 (カラー値128以上)はscreenで明るく、 暗い場合はmultiplyで暗く描画されます。 ※追加するの図形の色を 暗いときは明るく、 明るいときは暗くしてから計算するため、 screenまたはmultiply指定時と 同じ結果になりません。 | |||
soft-light | hard-lightと同様に 追加する図形の色が明るい場合は明るく、 暗い場合は暗くなりますが、 よりソフトに変化します。 | |||
difference | 明るい色から暗い色を減算します。 例: 青:00F - 赤:F00 → F0F 緑:008000 - 赤:FF0000 → FF8000 |
Non-separable blend modes
Non-separable blend modesは、RGB個別ではなく、全ての色要素の組み合わせで合成処理をおこないます。
指定値 | 意味 | 結果 | ||
---|---|---|---|---|
mix-blend-mode系 Non-separable blend modes | ||||
hue | 追加する図形の色相と、 既存の図形の彩度と明度で色を作成します。 | |||
saturation | 既存の図形の色相と明度と、 追加する図形の彩度とで色を作成します。 | |||
color | 追加する図形の色相と彩度と、 既存の図形の明度でカラーを作成します。 | |||
luminosity | 追加する図形の明度と、 既存の図形の色相と彩度で色を作成します。 |
補足:mix-blend-mode合成計算式
補足としてmix-blend-modeの合成処理における計算式をお伝えします。
計算式は、https://drafts.fxtf.org/compositing/#blendingnonseparableに掲載されているものを、JavaScriptに書き換えています。
一部を除き、関数は同じ引数を受け付けます。
関数( bc , sc )
bc : 既存図形の色要素配列 [ 赤 , 緑 , 青 ] 各 0 ~ 255
sc : 追加する図形の色要素配列 [ 赤 , 緑 , 青 ] 各 0 ~ 255
Separable blend modes
multiply
const multiply = ( bc , sc ) => bc.map( ( e , i ) => Math.round(e * sc[i] / 255.0 ) );
screen
const screen = ( bc , sc ) => bc.map(
( e , i ) =>
Math.round( ( 1.0 - ( 1.0 - e / 255.0 ) * ( 1.0 - sc[i] / 255.0 ) ) * 255 )
);
overlay
const overlay = ( bc , sc ) => bc.map(
(e,i) => ( e <= 127) ? multiply( sc[i] , 2.0 * e )
: screen( sc[i] , 2.0 * e - 255.0 )
);
darken
const darken = ( bc , sc ) => bc.map( ( e , i ) => Math.min( e , sc[i] ) );
lighten
const lighten = ( bc , sc ) => bc.map( ( e , i ) => Math.max( e , sc[i] ) );
color-dodge
const colorDodge = ( bc , sc ) => bc.map(
(e,i) => e === 0 ? 0 :
( sc[i] === 255 ? 255 :
Math.min( 255 , Math.round( e / ( 255 - sc[i] ) * 255 ) )
)
);
color-burn
const colorBurn = ( bc , sc ) => bc.map( ( e , i ) =>
255 - ( e === 1 ? 255 :
( sc[i] === 0 ? 0 :
Math.min( 255 , Math.round( ( 255 - e ) / sc[i] * 255 ) )
)
)
);
hard-light
const hardLight = ( bc , sc) => bc.map( ( e ,i ) =>
( sc[i] <= 127) ? multiply( e , 2.0 * sc[i] ) :
screen( e , 2.0 * sc[i] - 255.0 )
);
soft-light
const softLight = ( bc , sc) => bc.map( ( e , i ) => {
const [ b , s ] = [ e / 255 , sc[i] / 255 ];
const res = ( s <= 0.5 ) ?
b - ( 1.0 - 2.0 * s ) * b * ( 1.0 - b) :
b + ( 2 * s - 1 ) * (
( b <= 0.25 ? ( ( 16 * b - 12 ) * b + 4 ) * b :
Math.sqrt( b ) ) - b );
return Math.round( res * 255 );
});
difference
const difference = ( bc , sc) => bc.map( ( e , i ) => Math.abs( e - sc[i] ) );
exclusion
const exclusion = ( bc , sc) => bc.map( ( e , i ) =>
Math.round( e + sc[i] - 2 * e * sc[i] / 255)
);
Non-separable blend modes
共通関数 1
Non-separable blend modes処理で、共通的に使用している関数です。
引数は独自のものをとります。
const Lum = C => 0.3 * C.red + 0.59 * C.green + 0.11 * C.blue;
const ClipColor = C => {
const L = Lum(C);
const n = Math.min( C.red, C.green, C.blue );
const x = Math.max( C.red, C.green, C.blue );
const f1 = cl => L + (((cl - L) * L) / (L - n));
const f2 = cl => L + (((cl - L) * (1 - L)) / (x - L));
const res = { ...C };
if( n < 0)
[ res.red , res.green , res.blue ] =
[ f1( res.red ) , f1( res.green ) , f1( res.blue ) ];
if( x > 1)
[ res.red , res.green , res.blue ] =
[ f2( res.red ) , f2( res.green ) , f2( res.blue ) ];
return res;
};
const SetLum = ( C , l ) =>{
const d = l - Lum( C );
const res = {};
[ res.red , res.green , res.blue ] =
[ C.red + d , C.green + d , C.blue + d ];
return ClipColor( res )
};
const Sat = C => Math.max( C.red, C.green, C.blue )
- Math.min( C.red, C.green, C.blue );
const SetSat = (C, s) => {
let sobj = [
{ name : "red", val : C.red },
{ name : "green", val : C.green },
{ name : "blue", val : C.blue }
];
sobj = sobj.sort( ( a , b ) => b.val - a.val );
const rObj = {};
if( sobj[0].val > sobj[2].val ) {
rObj.Cmid = (((sobj[1].val - sobj[2].val) * s) / (sobj[0].val - sobj[2].val))
rObj.Cmax = s;
} else {
rObj.Cmid = rObj.Cmax = 0;
}
rObj.Cmin = 0;
const res = {};
res[ sobj[0].name ] = rObj.Cmax;
res[ sobj[1].name ] = rObj.Cmid;
res[ sobj[2].name ] = rObj.Cmin;
return res;
};
共通関数 2
各関数で受け取った色配列を、共通関数 1で使用するオブジェクトに変換、または逆変換をおこなうオリジナル関数です。
// カラー値を0.0~1.0の範囲に変換し、オブジェクトにセット
const setColorObj = c => ({
red : c[0] / 255 ,
green : c[1] / 255 ,
blue : c[2] / 255
});
// カラー値を0~255の範囲に変換し、配列にセット
const restoreColorObj = c => [
Math.round( c.red * 255 ) ,
Math.round( c.green * 255 ) ,
Math.round( c.blue * 255 )
];
hue
const hue = ( bc , sc ) => {
const [ bcc , scc ] = [ setColorObj(bc) , setColorObj(sc) ];
return restoreColorObj( SetLum(SetSat(scc, Sat(bcc)), Lum(bcc)) );
};
saturation
const saturation = ( bc , sc ) => {
const [ bcc , scc ] = [ setColorObj(bc) , setColorObj(sc) ];
return restoreColorObj( SetLum(SetSat(bcc, Sat(scc)), Lum(bcc)) );
};
color
const color = ( bc , sc ) => {
const [ bcc , scc ] = [ setColorObj(bc) , setColorObj(sc) ];
return restoreColorObj( SetLum( scc , Lum(bcc)) );
};
luminosity
const luminosity = ( bc , sc ) => {
const [ bcc , scc ] = [ setColorObj(bc) , setColorObj(sc) ];
return restoreColorObj( SetLum( bcc , Lum(scc)) );
};
更新日:2023/01/30
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。