with the flow

WEBプログラマを目指すWEBデザイナーが書き綴る開発日誌のようなもの

Input type="radio"をCSSだけで装飾する

CSSだけで頑張るシリーズ、今回はradioボタンに挑戦。

FirefoxでもIE10でも非対応なので、完全にスマホ用です。
といってもプロダクションで使うのはまだ気が引けますね。
これをベースにfallbackをきちんと作れば、何とかいけるかもってレベル。。。

iOS6, Android4.X~ではうまく動くはず。
手持ちのAndroid2.XではうまくスタイルされたけどAndroidは端末によって描画が全然違うことがあるので微妙。

Firefox(23.0.1)では、選択されたとき中心に青い丸が出るはずが、黒になってしまう。
あと、全体的にデザインが意図したとおりになっていない。っていうか程通い...
やっぱりappearance属性のサポートはまだ発展途上といったところですね。。。

Mobile Safariのフォームからのfile uploadで、png24/32がjpegになってしまう

結論から。
iOS6から可能になった、ファイルをアップロードするinput type="file"ですが、
表題にもあるように、PNGを送信する前に強制的にjpegに変換されてから送信されるようです。

今のところ解決策は見つかっていません。
iOS7は未確認。解決してるといいなぁ...(遠い目)

ちなみにiOS6のChrome for iOSでも同じでした。
同じフォームのURLをPC(Chrome)で開き、アップロードすると、
正常にPNGのままアップされたので、コードやサーバの設定などが問題ではないと思います。。

いろいろググってみたんですが、そもそもあまり需要がないのか、
同じ現象自体が見つからない。。

iOS標準のキャプチャ(ホームボタン+電源ボタンで撮るやつ)って、
内部ではPNGで保存されてるので、
それをそのまま持ってきたいことってあるんですよね。。。

たとえば、きれいなグラデーションがかかった画面をキャプチャして、
それをアップロードすると、ブラウザ側で勝手にjpeg変換(=非可逆圧縮)されるので
ビミョーにノイズが乗ってしまうという。

※当然だけど、メールで送ったり、dropboxiTunesで同期したりするとPNGのまま取得できます。

なんかバグチケットみたいなエントリーになっちゃいましたが、
はじめ自分のコードが悪いのかと思って散々調べた挙句の結果がこれだったので
一応共有しておきます。

Androidの2.X系だけでJavaScriptのParseErrorが起こった時の対策

PC向けだけに作製していた画面を、
スマホ向けに修正していた時にぶち当たった問題。
解決したので共有します。

Android2.Xでだけ、JavaScriptの特定の行でParseErrorが起こって、
それ以降のスクリプトが読み込まれないという現象が。。

8/2発表の最新の統計でも、Android 2.X系のシェアは33%くらいあるし、無視できない問題。

発生した端末は、
REGZA Phone T01C(Android2.2)、Androidエミュレータ2.3.3以下など。
但し、Arrows IS12F(Android2.3.5)では発生しなかった。
なのでAndroid2.3.3以下で発生すると思われます。

エラー内容はこんな感じ。

Console: SyntaxError: Parse error http://example.com/hoge.js:210

hoge.jsの210行目で構文エラーだと怒られる。
そして、このエラーはPC版ブラウザ(IE7とかも含む)やiOS,Android2.3.5~では起こりません。

調べると、結局は
JavaScript予約語をプロパティとしてオブジェクトの中で使うと、
パースエラーになる(ことがある)

ということでした。

例えば、グローバルオブジェクト"APP"の中に色々と突っ込んで使う場合、
画面上のタブパネルの切り替えを実行する関数を定義するために、
APP.panels.switchなんてうっかり定義しちゃうとそこでパースエラー。
Androidのパーサが貧弱ってことなのか。。。

var APP = {};
APP.init = function(){ ... };
APP.panels = {};
APP.panels.switch = function(){ ... };//ここでパースエラー
APP.panels.close =  function(){ ...//この行以降は読み込まれない

普通予約語ってのはJS書いてりゃ覚えてくるので、
好き好んでプロパティ名に使うはずもないんですが、
文脈の中でうっかり使ってしまうとこうなります。

しかも、予約語の種類によっては発生しないこともあるのでたちが悪い。
例えば、finalやboolean等は発生せず、switch, true, whileなどでは発生します。

とはいっても、うっかり使ってしまうのってswitchくらいじゃないかな。。。
タブとかClass名を変更するからswitch、みたいな感覚で。
PCサイトだとエラーにならないからスマホで動作確認してないと気づかないし、
なるべく短い名前にしたくて使っちゃいそう。

今回の場合だと、"APP.panels.switchTo"とかすれば、文脈もあんまり変えずに解決できますね。

この問題って、既存のjQueryプラグインとかでも引っかかってるのありそう。。。
地道に気をつけていくしかない感じですね。

ということで、
Android2系で原因不明なパースエラーが起こっている時には
予約語をうっかり使ってしまってないか、確認してみてください。

CSSのappearanceがFirefoxで効かない場合の解決策

# 2014.8.25 重要な追記
このハックは、今年4月あたり(FirefoxNightly 31?)から効かなくなってます。。
元記事 https://gist.github.com/joaocunha/6273016 でも、どうするよ的な議論&試行錯誤が交わされていますが、:-moz-any(hoge):before プロパティを使った方法で消せるのを見つけた人がいるようです(FF3.6~限定)。
http://jsbin.com/pozomu/4/edit

なんだか素直にJS使った方がいい気がしてきますね。

----

今までは、フォーム内の特定の部品(radioボタン、select要素など)のデザインを好きなように変更しようと思ったら、
JavaScript(有名所ではuniform.jsとか…)を使って色々と手を加えてやらなければいけませんでした。

でも最近、Chrome等のモダンブラウザでは、
"-webkit-appearance: none;"等と書く事で
ブラウザデフォルトのSelect要素のデザインをリセットできるので、
自由にデザインが変更できるようになってきました。

便利な時代になりましたね。

※appearanceは、正確にはブラウザが独自に描画するデフォルトUIをCSSで変更できるプロパティです。
→ https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance

ただ、このプロパティはまだ実験段階。
特定の要素にしか効かなかったり、細かいバグが有ったりと、いろいろ発展途上です。

特に最近気になっていたのがSelect要素でのバグ。
appearanceを"none"に設定してやれば、あとは自由に装飾し放題!と思いきや、
Firefoxだけ、デフォルトのSelect要素の右側に表示される下矢印みたいなのが
残ったままになってしまいます。
今日はそれを解消する方法を見つけたので共有します。

https://gist.github.com/joaocunha/6273016

↑ここ見れば一発なんですが、

select {
	text-indent: .01px;
	text-overflow: "";
}

をつけると、Firefoxで出ていた矢印がうまい具合にコレが消えてくれるとのこと。
Mozillaのサイトにもあるように、領域をはみ出した要素をどうするか、というのを指定する"text-overflow"は、カスタムで空文字を指定することが出来ます(現時点ではGeckoだけがサポート)。
Firefoxでは、Select要素の下矢印の扱いはインラインテキストの1文字と同等のようで、
text-indentで見た目には変化のない0.01pxを指定すると「下矢印が領域をはみ出した」と判断して、text-overflowの指定ルールに従って省略してくれるという仕組みのようです。
なんだかかなり不安定なハックな気もしますが。。。

実際に自分でもやってみました(Firefoxで見た時だけ分かるサンプルです)。


Firefox開くのめんどいという方は以下のキャプチャ画像をどうぞ。

f:id:mhstid:20130829125022p:plain

こんなかんじで、text-indent, text-overflowを付けた右側のSelect要素の下矢印が
ちゃんと消えてくれているのが分かります。

ただ、こういうハックは後で何が起こるか分からないので、
UAFirefoxを判定し、htmlタグに"firefox"というクラスを付けてから、
.firefox .uiSelect select {
text-indent: .01px;
text-overflow: "";
}

とやったほうが良いですね。
あと、~IE10ではappearanceプロパティ自体に対応していないので、
このテクニックはスマホ案件で使うこと前提ということで。。。

Canvasで雪っぽい表現を作る

Canvasの勉強をしたかったので、以下のコードを参考に、
雪が降ってるっぽい表現を作ってみました。
http://thecodeplayer.com/walkthrough/html5-canvas-experiment-a-cool-flame-fire-effect-using-particles

要するに、canvas
1:画面をすべて黒く塗り潰す
2:その上に、前回から位置等をずらした指定個数分の丸を描く

っていうのを猛烈な勢いでやってるだけです。

また、ctx.createRadialGradientを使って、
中心から「ランダムな透明度の白→透明」の円を描くことで、ぼかしと同じ効果を表現できています。

var gradient = ctx.createRadialGradient(p.location.x, p.location.y, 0, p.location.x, p.location.y, p.radius);
gradient.addColorStop(0, "rgba("+p.r+", "+p.g+", "+p.b+", "+p.opacity+")");
gradient.addColorStop(1, "rgba("+p.r+", "+p.g+", "+p.b+", 0)");
ctx.fillStyle = gradient;


ポイントはこの辺り。

function draw() {
  ctx.globalCompositeOperation = "source-over";//1
  ctx.fillStyle = "rgb(20,20,40)";
  ctx.fillRect(0, 0, W, H);//2
  ctx.globalCompositeOperation = "lighter";//3
  for(var i = 0; i < particles.length; i++){
  (… 雪を個数分描画する処理 …)//4,5
  }
}
setInterval(draw, 40);

上記を踏まえて、draw()処理の流れを詳細にすると

1:画面をsource-over(塗りつぶし)モードにする
2:すべて黒く塗り潰す
3:画面をlighter(Photoshop風に言うとオーバーレイ?)モードに切り替え
4:指定個数分の丸を描く
5:次の描画位置をセット

です。
globalCompositeOperation は「合成方法」です。
illustratorの「パスファインダ」にPhotoShopの「描画モード」の考え方を少し足したみたいなもんでしょうか。
別々にcanvas上へ描画した要素やイメージ同士が重なった時にどう表現するかを設定できます。
http://www.htmq.com/canvas/globalCompositeOperation.shtml


要するに、「画面をすべて黒く塗り潰す」時には、画面をsource-over(塗りつぶし)モードにし、雪同士が重なった時には、少し白っぽく見せたいので"lighter"の指定を使っているということですね。
今回は白でやったので"lighter"はわかりにくいかもしれませんが、赤とか、冒頭のサンプルにあるように7色入り乱れてやる場合は"lighter"の指定が生きてきます。

毎回塗りつぶすんじゃなくて
ctx.clearRect(0,0,W,H)って本当に消しても良さそうな気もしますが。
冒頭の参考コードは塗りつぶすやり方でした。
描いたのを消すのと黒く塗り潰すのとどちらが効率がいいんだろうか。。
と疑問に思ったので一応作ってみた。

単色で塗りつぶさないので、背景に雪国の写真とか、
今回みたくグラデーションとか、任意のものがcanvasに頼らずに使えるのがいいところですね。
こっちのほうが良いかもしれない。

input type="file"をCSS3で装飾 改善版 IE7〜対応。

すごく昔に書いた、input type="file"をCSSとJavaScriptで綺麗に装飾するものの改善版を作りました。

Chrome23.0.1, Firefox17, Opera12, IE9,8,7で正常に表示されるのを確認。

■内容
・「参照」ボタンを押しても、textboxを押しても参照ダイアログが起動。
・ファイルを参照するとtextbox内にファイル名が入る。ファイル名が長い場合は「...」と省略される。
・「アップロード」ボタンがあった場合で作ってみた。押しても何も起こりませんが。
・画面内に複数入れたい場合は、"uploader"を複数入れることで対応可能。
JavaScriptが無効な状態にも対応。無効な環境では普通のブラウザデフォルトのinput type="file"が出現し、  アップロードボタンの左に並びます。
IEは画像作ってないので必要であれば足してください。 ※IEのfilterでグラデ作ろうかとも思ったけど、あんまりすきじゃないので使ってません。

■ポイント
IE対策。trigger()で、潰して見えなくしたinput=fileをJavaScript経由でクリックさせる方法は IEには通用しないので(無効にされる)、力技でtextboxの上に透明にして重ねる方法を取りました。 ただ、IEのデフォルトのinput=fileは、

・参照したファイル名が入るテキストbox部分をクリックしても参照ダイアログが立ち上がらない。
・中身のテキストも修正可能

という罠があるので、font-sizeを大きくすることで「参照...」と書いてあるボタンのサイズを調整、 overflow: hiddenでカットという荒業で乗り切りました。
フェイクのinput要素(".text")は、 contenteditable="false" readonly="true"を付けることでタブキーなどでのフォーカスをお断りしています。

HTML

<div class="uploader no-js">
	<input type="file" size="25">
	<div class="fakeFile" contenteditable="false" readonly="true">
		<p class="text"></p>
		<a href="javascript: void 0;" class="uiBtn browse" title="参照ボタンをクリックし写真選択後、アップロードボタンを押してください。">参照...</a>
	</div>
	<a href="javascript:void 0;" id="upload" class="uiBtn upload">アップロード</a>
</div>

CSS

body {
    background: #272727;
    font-size: 12px;
    font-family: "メイリオ",Meiryo,"MS Pゴシック","MS PGothic",Tahoma,Verdana,Arial,'Hiragino Kaku Gothic Pro',sans-serif;
}
.uploader {
	position: absolute;
    top: 50%;
    left: 50%;
    width: 402px;
    margin: -15px 0 0 -200px;
}

/* // Fake file uploader
------------------------- */
.fakeFile {
	position: relative;
	float: left;
	height: 25px;
	overflow: visible;
	cursor: pointer;
	z-index: 1;
}
.no-js .fakeFile { display: none; }
.fakeFile .text {
	float: left;
	width: 190px;
	height: 19px;
	margin: 1px 10px 0 0;
	padding: 1px 1px 1px 5px;
	border: 1px solid #000;
	border-radius: 2px;
	background: #161616;
	color: #ccc;
	line-height: 20px;
	white-space: nowrap;
	text-shadow: 0 0 -1px #333;
	overflow: hidden;
	text-overflow: ellipsis;
	cursor: pointer;
	box-shadow: inset 1px 1px 1px rgba(0,0,0,0.5), 1px 1px 0 rgba(255,255,255,0.1);
}
.uploader input[type="file"] {
	position: absolute;
	display: block;
	border: 0;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	z-index: 1;
}
.uploader.isIE input[type="file"] {
	position: absolute;
	left: 0;
	top: 0;
	width: 281px;
	height: 26px;
	margin: 0;
	font-size: 52px;
	opacity: .0;
	filter: alpha(opacity=0);
	-ms-filter: "alpha(opacity=0)";
	cursor: pointer;
	z-index: 2;
}
.uploader.no-js input[type="file"] { width: auto; height: auto; display: inline; }
/* IE7 */
*:first-child+html .uploader input[type="file"] { left: -7px; font-size: 72px; }

/* // UI Parts: button
------------------------- */
.uiBtn:link, .uiBtn:visited {
    display: inline-block;
	padding: 6px 0;
	border: 1px solid #070a0d;
	border-radius: 2px;
	background: #363636;
	background: -moz-linear-gradient(top, #444, #262626);
	background: -webkit-linear-gradient(top, #444, #262626);
	background: -o-linear-gradient(top, #444, #262626);
	background: linear-gradient(to bottom, #444, #262626);
	color: #ddd;
	text-align: center;
	text-decoration: none;
    text-shadow: 0 -1px 0 #111;
	box-shadow: inset 0 0 2px rgba(100,100,100,0.5), 1px 1px 0 rgba(0,0,0,0.15);
    overflow: hidden;
    zoom: 1;
}
.uiBtn:hover, .uiBtn.hover {
	background: #444;
	background: -moz-linear-gradient(top, #525252, #2e2e2e);
	background: -webkit-linear-gradient(top, #525252, #2e2e2e);
	background: -o-linear-gradient(top, #525252, #2e2e2e);
	background: linear-gradient(to bottom, #525252, #2e2f30);
	color: #fff;
}
.uiBtn:active, .uiBtn.active {
	border-color: #000;
	background: #111;
	background: -moz-linear-gradient(top, #262626, #444);
	background: -webkit-linear-gradient(top, #262626, #444);
	background: -o-linear-gradient(top, #262626, #444);
	background: linear-gradient(to bottom, #262626, #444);
	color: #999;
	box-shadow: inset 1px 1px 1px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.1);
}

/* uiBtn overwrite  */
a:link.browse, a:visited.browse,
a:link.upload, a:visited.upload {
	display: block;
	float: left!important;
	width: 70px;
	height: 11px!important;
	line-height: 12px;
	border-radius: 0;
}
a:link.browse, a:visited.browse {
	border-top-left-radius: 2px;
	border-bottom-left-radius: 2px;
}
a:link.upload, a:visited.upload {
	width: 120px;
	margin-left: -1px;
	border-left-color: #161616;
	border-top-right-radius: 2px;
	border-bottom-right-radius: 2px;
	box-shadow: inset 0 0 2px rgba(100,100,100,0.5), inset 1px 0 0 rgba(150,150,150,0.3), 1px 1px 0 rgba(0,0,0,0.15);
}
a:active.upload { box-shadow: inset 1px 1px 1px rgba(0,0,0,0.3),1px 1px 0 rgba(255,255,255,0.1); }
.no-js a:link.upload, .no-js a:visited.upload {
    display: inline-block;
    float: none!important;
    border-radius: 2px;
    box-shadow: inset 0 0 2px rgba(100,100,100,0.5), 1px 1px 0 rgba(0,0,0,0.15);
}

JavaScript

$(function(){
    $(".fakeFile").each(function(){
        var $this = $(this),
            $browse = $this.children(".browse"),
            $file = $this.prev("input");
        
        //JavaScript無効の状態ではno-jsを取る。Modernizrを入れていて<html>に".no-js"が付けている環境では、この記述は不要。
        $this.parent().removeClass("no-js");
        
        //IEはtrigger()でのクリックが効かないので対策。IE判定は横着してます--;
        if(/*@cc_on!@*/false) {
            $file
                .parent().addClass("isIE")
                .end()
                .bind({
                    click: function(e){ $(this).blur(); },
                    mousedown: function(){ $browse.addClass("active"); },
                    mouseup: function(){ $browse.removeClass("active"); },
                    mouseover: function(){ $browse.addClass("hover").tipsy("show"); },
                    mouseout: function(){ $browse.removeClass("hover active").tipsy("hide"); },
                    change: function(){ $(this).next().children(".text").text($(this).val()); }
                });
        } else {
            //IE以外のブラウザではtriggerで。
            $this.bind({
                click: function(e){ $file.trigger("click"); },
                mousedown: function(){ $browse.addClass("active"); },
                mouseup: function(){ $browse.removeClass("active"); },
                mouseover: function(){ $browse.addClass("hover").tipsy("show"); },
                mouseout: function(){ $browse.removeClass("hover active").tipsy("hide"); }
            });
            $file.change(function(){
                $this.children(".text").text($(this).val());
            });
        }
    });
});