Photoshopを使って手軽にPC向けプロトタイピングをするツールを作った

プロトタイピングツールは色々あるけど、「リンクがあって画像切り替えができる」ってくらいの必要最低限なものをサクッと作れるツールが欲しかったのでjsxで作りました。

※もし「良いツール知ってるよ!」という方がいらっしゃればご教授頂けると幸いですm(_ _)m


今回のツールで実現したかったこと

  • 一番使用頻度の高いグラフィックツールであるPhotoshopでリンクの設定を行う
  • リンクからの画像切り替えを同一ページで行う(ページ遷移しない)
  • モダンブラウザ対応
  • お手軽

要件出ししてみるとこんなもん。

今の自分が実現できる方法だとJSXを使っての作業。(本当はエクステンション化までしたかったけど完成まで時間かかりそうだし、早く作りたかったので今回はJSXで。今後の拡張でエクステンション化するかも…)

そして上記を実現したものが以下。

ダウンロード

今回紹介するスクリプトは GitHub で公開しています。

ここから下にも解説やソースを書きますが、基本 GitHub(上記URL)で書いていることなので、リンク先から直接ファイルをダウンロードして実行してみるのが一番早いかもしれません。

ソースのダウンロードは画像の場所の「Clone or download」「Download ZIP」から行えます。
注意にも書きましたが、本プログラムの使用は自己責任でお願いいたします

使用法

  • 「SwitchImageMapAndImgGenerator.jsx」をダブルクリックか Photoshop 内へドラッグ&ドロップして実行

JSXファイルと同階層に「sampleA.psd」と「sampleB.psd」を用意しています。 それぞれ、特定のルールに基づいて作成されたpsdですので、どちらかをPhotoshopで開き、JSXファイルを実行すると実行日時のフォルダが作成され、中にプロトタイピング用のhtmlファイル一式と画像が生成されます。 生成されたindex.htmlをお好きなブラウザで開いて結果を確認できれば成功です。

仕様 (主にPhotoshopドキュメント内の構成に関するルール)

  • SwitchImageMapAndImgGenerator.jsx がメインの実行ファイル
  • JSX処理の対象となるドキュメントはPhotoshop上でJSX実行時にアクティブになっているドキュメントである
  • ※A が画像として書き出される (デフォルトでは特定の値は「@@@」、画像形式はjpg – png、gifにも切り替え可)
  • ※A は名前に関係なくレイヤーパネル内の上から順に 「1、2、3、4、5、、、」 と処理的に番号付けされる (なので「@@@1、@@@2、、、」と名前を付けると分かりやすい)
  • ※B はリンクを設置したいレイヤーセット内に作成する (矩形が望ましい)
  • ※B の名前は画像として書き出される「※A」の順番と紐付けると、JSX処理後に生成されるhtml上でその画像に対してのリンクとなる
  • 基本となるhtml等は「_template」からコピーされる
  • 画像のリンクはhtmlのイメージマップ、画像の切り替えはJavaScriptで行っている
  • Photoshopから生成される画像やリンクの情報は「js/include.js」に記載されている

上記使用略称: ※A … 第一階層にある特定の値を持つレイヤーセット
上記使用略称: ※B … 第一階層内、第二階層にあるシェイプ

簡単に要点をまとめると、

  • 第一階層にある、名前の先頭に「@@@」が付いたレイヤーセットはjpg書き出しされる
  • 上記の「@@@」のレイヤーセット内の第二階層にあるシェイプの位置情報と名前を利用してリンクを作成する(リンク先はレイヤーソート順)

という感じです。

百聞は一見に如かず。以下実行動画をご覧ください。

ツール実行例動画

例01

付属psd「sampleA.psd」「sampleB.psd」のプロトタイピングデータを作成・実行。
ドキュメントを開いていない時、ドキュメント内のデータ構成が正しくない時のエラーも実行。

例02

一から「sampleB.psd」と同じようなリンクを作成する手順を紹介。
上記のルールを読みつつこの動画を見れば大体ルールは分かるかと。

SwitchImageMapAndImgGenerator.jsx のソース

#target photoshop
app.bringToFront();

/* ----------------------------------------------------------------------------------------------
 * PhotoshopJSX-SwitchImageMapAndImgGenerator
 * ----------
 * Author: Tsutomu Takanashi
 * Copyright (c) 2017+ Tsutomu Takanashi
 * 
 * Project home:
 * 	https://github.com/t-nashi/PhotoshopJSX-SwitchImageMapAndImgGenerator
 * 
 * Blog page:
 * 	http://www.koreyome.com/web/photoshop-jsx-switchimagemapandimggenerator/
 * 
 * This software is released under the MIT License:
 * 	https://opensource.org/licenses/mit-license.php
 * ---------------------------------------------------------------------------------------------- */

//-------------------------------------------------------------
// PHOTOSHOP DOCUMENT SETTING
//-------------------------------------------------------------
//画像として書き出すレイヤーセット名に付ける印
var FirstHierarchyTarget = "@@@";

//書き出しの画像の種類を設定		※jpg(0-100), png8(0-256), png, gif(0-256)
var fileExt = "jpg";
var fileQuality = 100;

//-------------------------------------------------------------
// GENERAL SETTING
//-------------------------------------------------------------
var _script			= $.fileName;
var _root			= File($.fileName).parent + "/";
var _scriptName		= File($.fileName).name;

var FirstLen = FirstHierarchyTarget.length;
var KindOfLayer = "LayerKind.SOLIDFILL";

var docMaking1 = false;
var docMaking2 = false;

var arrSaveImages = [];
var arrJumpToName = [];
var arrCoordsVal = [];
var arrJumpName = [];

var days = ["sun","mon","tue","wed","thu","fri","sat"];
var dObj = new Date();
var y = dObj.getFullYear();
var m = dObj.getMonth()+1;
var d = dObj.getDate();
var yb = dObj.getDay();
var h = dObj.getHours();
var mi = dObj.getMinutes();
var s = dObj.getSeconds();
var ms = dObj.getMilliseconds();
if(String(m).length==1) m="0"+m;
if(String(d).length==1) d="0"+d;
if(String(h).length==1) h="0"+h;
if(String(mi).length==1) mi="0"+mi;
if(String(s).length==1) s="0"+s;
var makeFolderName = y+""+m+""+d+""+days[yb]+"_"+h+""+mi+""+s;
makeFolderName = _root + makeFolderName;
var exportFileName = makeFolderName + "/js/include.js";

var exportText = "";
var existsFileText = "";

if(app.documents.length == 0){
	alert("Not find document!!");
}else{

	try {
		targetLayerSetVisibleOFF(activeDocument);
		if(docMaking1){
			templateCopy();
			targetLayerSetSaveAs(activeDocument);
			targetSpapeLayerInfoGet(activeDocument);
			if(docMaking2){
				textSave();
				alert("finish!");
			}else{
				alert("第二階層にshapeレイヤーが存在しませんでした。\n再度ドキュメントが適切な状態かご確認ください。");
			}
		}else{
			alert("第一階層に特定の特定文字列を含んだレイヤーセットがありませんでした。\n再度ドキュメントが適切な状態かご確認ください。");
		}
	}catch(e) {
		alert(e);
	}
}
//===============================================================================

function templateCopy(){
	makeFolder(makeFolderName + "/js");
	makeFolder(makeFolderName + "/images");
	makeFolder(makeFolderName + "/css");
	copyFile("index.html");
	copyFile("js/functions.js");
	copyFile("css/style.css");
}

function targetLayerSetVisibleOFF(doc){
	var flagEnd = false;
	var j = 0;
	var flag = true;
	var tabNum=0;
	var tabSet="";
	var iTraceNum=0;
	targetLayerSetVisibleOFF_inner(doc);
	function targetLayerSetVisibleOFF_inner(doc_inner){
		var ChildLyaers= doc_inner.layers;
		for (var i = 0; i < ChildLyaers.length; i++){
			if (ChildLyaers[i].typename == "LayerSet"){
				j++;
				tabSet="";
				for(var z=0; z<tabNum; z++) tabSet+="\t";
				if(tabNum==0){
					var SpriteStr = (ChildLyaers[i].name).substring(0, FirstLen);
					if(FirstHierarchyTarget == SpriteStr){
						ChildLyaers[i].visible = false;
						if(!docMaking1) docMaking1 = true;
					}
				}
				tabNum++;
				targetLayerSetVisibleOFF_inner(ChildLyaers[i]);
			}
			j++;
			tabSet="";
			if(flagEnd) flagEnd=false;
			if((i+1) == ChildLyaers.length) flagEnd=true;
		}
		tabNum--;
		j=0;
	}
}

function targetLayerSetSaveAs(doc){
	var flagEnd = false;
	var j = 0;
	var flag = true;
	var tabNum=0;
	var tabSet="";
	var iTraceNum=0;

	targetLayerSetSaveAs_inner(doc);
	function targetLayerSetSaveAs_inner(doc_inner){
		var ChildLyaers= doc_inner.layers;
		for (var i = 0; i < ChildLyaers.length; i++){
			if (ChildLyaers[i].typename == "LayerSet"){
				j++;
				tabSet="";
				for(var z=0; z<tabNum; z++) tabSet+="\t";
				if(tabNum==0){
					var SpriteStr = (ChildLyaers[i].name).substring(0, FirstLen);
					if(FirstHierarchyTarget == SpriteStr){
						ChildLyaers[i].visible = true;
						var exportPath = makeFolderName + "/images/";
						switch(fileExt){
							case "jpg":
								jpgExport_fullPath(exportPath, ChildLyaers[i].name, fileQuality);
								break;
							case "png8":
								png8Export_fullPath(exportPath, ChildLyaers[i].name, fileQuality);
								break;
							case "png":
								png24Export_fullPath(exportPath, ChildLyaers[i].name);
								break;
							case "gif":
								gifExport_fullPath(exportPath, ChildLyaers[i].name, fileQuality);
								break;
						}
						arrSaveImages.push(ChildLyaers[i].name);
						ChildLyaers[i].visible = false;
					}
				}
				tabNum++;
				targetLayerSetSaveAs_inner(ChildLyaers[i]);
			}
			j++;
			tabSet="";
			if(flagEnd) flagEnd=false;
			if((i+1) == ChildLyaers.length) flagEnd=true;
		}
		tabNum--;
		j=0;
	}
}

function targetSpapeLayerInfoGet(doc){
	var flagEnd = false;
	var j = 0;
	var flag = true;
	var tabNum=0;
	var tabSet="";
	var iTraceNum=0;
	var parentLayerSet = "";

	targetSpapeLayerInfoGet_inner(doc);
	if(docMaking2){
		exportText += "var exportImages = [];\n";
		exportText += "exportImages.push('');\n";
		var jsFileExt = "";
		if(fileExt == "png8"){
			jsFileExt = "png";
		}else{
			jsFileExt = fileExt;
		}
		for (var i=0; i<arrSaveImages.length; i++) {
			exportText += "exportImages.push('images/"+arrSaveImages[i]+"."+jsFileExt+"');\n";
		}
		exportText += "\n\n";

		var varBaseName = "pageLinkAreaSet";
		var varText = "";
		var evalText = "";

		exportText += "var ";
		for (var i=0; i<arrSaveImages.length; i++) {
			var posFrom = FirstLen;
			var posTo = arrSaveImages[i].length;
			var arrDefaultStr = (arrSaveImages[i]).substring(posFrom, posTo);
			var j = i+1;
			if(j<arrSaveImages.length){
				varText += varBaseName + arrDefaultStr + ",";
			}else{
				varText += varBaseName + arrDefaultStr + ";";
			}
			evalText += "\teval(\'if(n=="+(i+1)+"){nextContentSet=pageLinkAreaSet" + arrDefaultStr + ";};\');\n";
		}
		exportText += varText + "\n";
		exportText += "var setNUM,setURL;\n";
		exportText += "\n\n";
		exportText += "function GetLinkAreaSet(n){\n";
		exportText += "\tvar nextContentSet=0;\n";
		exportText += evalText;
		exportText += "\treturn nextContentSet;\n";
		exportText += "}\n";
		exportText += "\n\n";
		var jsOutParagraph = "";
		for (var i=0; i<arrJumpToName.length; i++) {
			var SpriteLen = arrJumpName[i].length;
			var SpriteStr = (arrJumpToName[i]).substring(FirstLen, SpriteLen);
			if(jsOutParagraph != SpriteStr){
				if(i==0) {
					exportText += varBaseName + SpriteStr + " = \n";
				}else{
					exportText += ";\n\n" + varBaseName + SpriteStr + " = \n";
				}
			}else{
				if(i==0) {
				}else{
					exportText += "+\n";
				}
			}
			var posFrom = arrJumpName[i].length;
			var posTo = arrJumpToName[i].length;
			var arrDefaultStr = (arrJumpToName[i]).substring(posFrom, posTo);
			var j = i+1;
			if(j<arrJumpToName.length){
				exportText += '\'<area shape="rect" coords="'+arrCoordsVal[i]+'" onclick="SwitchImageMapAndImg('+arrDefaultStr+');">\'';
			}else{
				exportText += '\'<area shape="rect" coords="'+arrCoordsVal[i]+'" onclick="SwitchImageMapAndImg('+arrDefaultStr+');">\';\n';
			}
			jsOutParagraph = (arrJumpToName[i]).substring(FirstLen, SpriteLen);
		}
	}

	function targetSpapeLayerInfoGet_inner(doc_inner){
		var ChildLyaers = doc_inner.layers;
		for (var i = 0; i < ChildLyaers.length; i++){
			if (ChildLyaers[i].typename == "LayerSet"){
				j++;
				tabSet="";
				for(var z=0; z<tabNum; z++) tabSet+="\t";
				if(tabNum==0){
					var SpriteStr = (ChildLyaers[i].name).substring(0, FirstLen);
					if(FirstHierarchyTarget == SpriteStr){
						parentLayerSet = ChildLyaers[i].name;
					}
				}
				tabNum++;
				targetSpapeLayerInfoGet_inner(ChildLyaers[i]);
			}
			j++;
			tabSet="";
			for(var z=0; z<tabNum; z++) tabSet+="\t";
			if(1 <= j && j < i){
			}else{
				if(ChildLyaers[i].kind==KindOfLayer && tabSet.length==1){
					var bounds = ChildLyaers[i].bounds;
					arrJumpName.push(parentLayerSet);
					arrJumpToName.push(parentLayerSet + ChildLyaers[i].name);
					arrCoordsVal.push(parseInt(bounds[0], 10) + "," + parseInt(bounds[1], 10) + "," + parseInt(bounds[2], 10) + "," + parseInt(bounds[3], 10));
					if(!docMaking2) docMaking2 = true;
				}
			}
			if(flagEnd) flagEnd=false;
			if((i+1) == ChildLyaers.length) flagEnd=true;
		}
		tabNum--;
		j=0;
	}
}

function copyFile(f){
	var srcFileObj = new File(_root + "_template/" + f);
	var dstFileObj = new File(makeFolderName + "/" + f);
	srcFileObj.copy(dstFileObj);
}

function jpgExport_fullPath(fullPath, fileName, qualityVal) {
	var doc = app.activeDocument;
	doc.changeMode(ChangeMode.RGB);
	var options = new ExportOptionsSaveForWeb();
	options.quality = qualityVal;
	options.format = SaveDocumentType.JPEG;
	options.optimized = false;
	options.interlaced = false;
	var ext = '.jpg'
	var saveName = new File(fullPath + fileName + ext);
	doc.exportDocument(saveName, ExportType.SAVEFORWEB, options);
}

function png8Export_fullPath(fullPath, fileName, colorVal){
	var doc = app.activeDocument;
	var pngOpt = new ExportOptionsSaveForWeb;
	pngOpt.format = SaveDocumentType.PNG;
	pngOpt.PNG8 = true;
	pngOpt.colors = colorVal;
	var ext = '.png'
	var saveName = new File(fullPath + '/' + fileName + ext);
	doc.exportDocument(saveName, ExportType.SAVEFORWEB, pngOpt);
}

function png24Export_fullPath(fullPath, fileName){
	var doc = app.activeDocument;
	pngOpt = new PNGSaveOptions();
	pngOpt.interlaced = false;
	var ext = '.png'
	var saveName = new File(fullPath + '/' + fileName + ext);
	doc.saveAs(saveName, pngOpt, true, Extension.LOWERCASE);
}

function gifExport_fullPath(fullPath, fileName, qualityVal) {
	var doc = app.activeDocument;
	doc.changeMode(ChangeMode.RGB);
	var options = new ExportOptionsSaveForWeb();
	options.quality = qualityVal;
	options.format = SaveDocumentType.COMPUSERVEGIF;
	options.interlaced = false;
	options.format = SaveDocumentType.COMPUSERVEGIF;
	options.colorReduction = ColorReductionType.ADAPTIVE;
	options.colors = qualityVal;
	var ext = '.gif';
	var saveName = new File(fullPath + '/' + fileName + ext);
	doc.exportDocument(saveName, ExportType.SAVEFORWEB, options);
}

function makeFolder(f){
	var folderName = _root + f;
	folderObj = new Folder(folderName);
	if(!folderObj.exists){
		folderObj.create();
	}else{
	}
}

function textSave(){
	var fileObj = new File(exportFileName);
	if(fileObj.exists){
		fileObj.open("r");
		existsFileText = fileObj.read();
		fileObj.close();
	}
	fileObj.open("w");
	fileObj.write("\n" + exportText + "\n" + existsFileText);
	fileObj.close();
}

一応ソースも掲載。

最後に

本ツールを作成して日が浅く、まだそんなに検証も出来ていないので何かあればご意見いただけると嬉しいです。
スマホ対応とかも考えてはいますが、そこら辺はすでに優秀なツールが数多くありそうなので需要とやる気次第かなと思っています。

以上、最後まで読んでいただき ありがとうございますm(_ _)m

Share