自作レバーレスのあれこれ [レイアウト編]
はじめに
「レバーレスは、自由なのです!ノれるのです!カッコいいのです!!ギュイーンとソウルがシャウトするのです!!」
※注意
レバーレスアケコンは自作するよりも買ったほうがお手軽かつコスト面でも優れています
自作はオススメしません
自作は自己満足、自己責任です
レイアウトについて
自作の最大の利点は自分の好きなようにボタンをレイアウトできること
奇妙奇天烈なスタイルも思いのまま
使用ツール
レイアウトに使用するツールは以下の3つ
誰でもどこでも無料
Google スライド
ボタン配置のデザインに使用する
そのままA4用紙に印刷して穴あけガイドにする
Google スプレッドシート
スライドの各ボタンの座標等を設定する
GP2040-CE編にて説明するConfigファイルのボタンレイアウトを設定する
Google Apps Script
スプレッドシートにて設定した値をもとにスライドにボタンオブジェクトを自動配置する
Configファイルのボタンレイアウトをlogとして出力する
完成イメージ
スライド、スプレッドシート、GASを使用すれば自分好みのボタン配置が数秒で完成します
手順
1.Google スライド
1.新しいプレゼンテーションを作成から空白のプレゼンテーションを選択する
2.ファイル > ページ設定から A4用紙サイズ(29.7cm x 21.0 cm)に設定する
3.レイアウトを空白に設定する
4.挿入 > 図形 > ドーナツを選択する
5.ドーナツを挿入し、内径を極力小さくしたものをボタン数だけコピペする
(サンプルの場合はディスプレイを含め19個)
6.ドーナツ同様に長方形を挿入する
スライドでの作業はここで一旦終了
2.Google スプレッドシート
1.新しいスプレッドシートを作成から空白のスプレッドシートを選択する
2.項目を以下のように設定する
No.
各ボタンの通し番号
BTN_NAME
ボタン名
ラベルとして使用
XINPUT_NAME
XInputで使用するボタン名
GPIO_PIN
ボタンが接続されているGPIOピンの番号
X_POS
スライドのボタンX座標
REF_SIZE * X_OFF
Y_POS
スライドのボタンY座標
REF_SIZE * Y_OFF
REF_SIZE
ボタンの基準サイズ
X_OFF
REF_SIZE * X_OFF でスライドのX座標
Y_OFF
REF_SIZE * Y_OFF でスライドのY座標
COLOR
スライドのボタンの色
CFG_ATTR
ボタンの設定属性
DIR = 方向キー, BTN = ボタン, DSP = ディスプレイ
CFG_SIZE
Configファイルのボタンレイアウトサイズ
CFG_AB
Configファイルのボタン全体レイアウト
A = 左, B = 右
CFG_REF_SIZE
Configファイルのボタン基準サイズ
CFG_X
ConfigファイルのボタンX座標
X_OFF * 2 * CFG_REF_SIZE + CFG_REF_SIZE
CFG_Y
ConfigファイルのボタンY座標
Y_OFF * 2 * CFG_REF_SIZE + CFG_REF_SIZE
3.アドレスバーのID部分をメモっておく
スプレッドシートでの作業はここで一旦終了
3.Google Apps Script
1.さきほど作成したスライドに戻る
2.拡張機能 > Apps Script を選択する
3.無題のプロジェクトが開いたら以下のソースをコピペする
※ 以下の【fileId】に先ほどメモったスプレッドシートのIDを入れる ※
// スプレッドシートからデータを取得する
function getSpreadsheetData_() {
// ※※※※※※ 作成するボタン配置によってIDを変える ※※※※※
const fileId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const sheet = SpreadsheetApp.openById(fileId).getSheets()[0];
return sheet.getDataRange().getValues();
}
ソース本体
function ButtonSetup() {
initializeButtonSetup_();
}
// スライド上のShapeの座標をログに出力する
function logShapeCoordinates() {
const presentation = SlidesApp.getActivePresentation();
const slide = presentation.getSlides()[0];
const data = getSpreadsheetData_();
const config = initializeConfig_();
slide.getShapes().forEach(shape => {
const shapeName = shape.getDescription();
const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
if (["DIR", "BTN"].includes(elementType)) {
const tmpConfig = generateShapeConfig_(shape, shapeName, data);
if (getCellValue_(data, shapeName, "CFG_AB") === "A") {
config.tmpA += tmpConfig;
} else {
config.tmpB += tmpConfig;
}
}
});
finalizeConfigStrings_(config);
logConfig_(config);
}
// ShapeのX座標とY座標を使って、座標を含むtmpConfig文字列を生成する
function generateShapeConfig_(shape, shapeName, data) {
const currentX = pointsToCm_(shape.getLeft());
const currentY = pointsToCm_(shape.getTop());
const refSize = getCellValue_(data, shapeName, "REF_SIZE");
const cfgRefSize = getCellValue_(data, shapeName, "CFG_REF_SIZE");
const cfgX = (currentX / refSize) * 2 * cfgRefSize + cfgRefSize;
const cfgY = (currentY / refSize) * 2 * cfgRefSize + cfgRefSize;
return generateTempConfigWithCoordinates_(shapeName, data, Math.round(cfgX), Math.round(Math.round(cfgY)));
}
// pointsをcmに変換する
function pointsToCm_(points) {
return points / (72 / 2.54);
}
// 座標を含むtmpConfig文字列を生成する
function generateTempConfigWithCoordinates_(shapeName, data, confX, confY) {
const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
const size = getCellValue_(data, shapeName, "CFG_SIZE");
const mask = `GAMEPAD_MASK_${shapeName}`;
return ` {GP_ELEMENT_${elementType}_BUTTON, {${confX}, ${confY}, ${size}, ${size}, 1, 1, ${mask}, GP_SHAPE_ELLIPSE}}, \\ \n`;
}
// スライドのボタン位置と設定を初期化する
function initializeButtonSetup_() {
const presentation = SlidesApp.getActivePresentation();
const slide = presentation.getSlides()[0];
const data = getSpreadsheetData_();
const buttonNames = extractButtonNames_(data);
const config = initializeConfig_();
ungroupSlideObjects_(slide);
assignButtonNamesToShapes_(slide, buttonNames);
processSlideShapes_(slide, buttonNames, data, config);
finalizeConfigStrings_(config);
groupAllSlideObjects_(slide);
logConfig_(config);
}
// スプレッドシートからデータを取得する
function getSpreadsheetData_() {
// ※※※※※※ 作成するボタン配置によってIDを変える ※※※※※
const fileId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const sheet = SpreadsheetApp.openById(fileId).getSheets()[0];
return sheet.getDataRange().getValues();
}
// Configを初期化する
function initializeConfig_() {
return {
tmpA: "#define DEFAULT_BOARD_LAYOUT_A \\\n { \\\n",
tmpB: "#define DEFAULT_BOARD_LAYOUT_B \\\n { \\\n"
};
}
// Config文字列を最終化する
function finalizeConfigStrings_(config) {
config.tmpA += " } \n";
config.tmpB += " } \n";
}
// データからボタン名を抽出する
function extractButtonNames_(data) {
const headers = data[0];
return data.slice(1).map(row => row[headers.indexOf("BTN_NAME")]);
}
// スライド上のボタンに名前を割り当てる
function assignButtonNamesToShapes_(slide, buttonNames) {
let nameIndex = 0;
slide.getShapes().forEach(shape => {
const shapeType = shape.getShapeType();
if (shapeType === SlidesApp.ShapeType.RECTANGLE) {
shape.setDescription("Frame");
} else if (shapeType === SlidesApp.ShapeType.DONUT && nameIndex < buttonNames.length) {
shape.setDescription(buttonNames[nameIndex]);
nameIndex++;
}
});
}
// スライド上のオブジェクトのグループ化を解除する
function ungroupSlideObjects_(slide) {
slide.getPageElements().forEach(pageElement => {
if (pageElement.getPageElementType() === SlidesApp.PageElementType.GROUP) {
pageElement.asGroup().ungroup();
}
});
}
// スライド上のShapeを処理する
function processSlideShapes_(slide, buttonNames, data, config) {
slide.getShapes().forEach(shape => {
processShape_(shape, buttonNames, data, config);
});
}
// Shapeを処理する
function processShape_(shape, buttonNames, data, config) {
const shapeName = shape.getDescription();
if (buttonNames.includes(shapeName)) {
configureButtonShape_(shape, shapeName, data, config);
} else if (shapeName === "Frame") {
adjustFrameShape_(shape, data);
}
highlightShapeBorder_(shape);
}
// ボタンを設定する
function configureButtonShape_(shape, shapeName, data, config) {
const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
configureShapeDimensions_(shape, data, shapeName);
configureShapeText_(shape, data, shapeName);
if (["DIR", "BTN"].includes(elementType)) {
const tmpConfig = generateTempConfig_(shapeName, data);
if (getCellValue_(data, shapeName, "CFG_AB") === "A") {
config.tmpA += tmpConfig;
} else {
config.tmpB += tmpConfig;
}
}
}
// Shapeのサイズと座標を設定する
function configureShapeDimensions_(shape, data, shapeName) {
shape
.setLeft(cmToPoints_(getCellValue_(data, shapeName, "X_POS")))
.setTop(cmToPoints_(getCellValue_(data, shapeName, "Y_POS")))
.setWidth(cmToPoints_(getCellValue_(data, shapeName, "REF_SIZE")))
.setHeight(cmToPoints_(getCellValue_(data, shapeName, "REF_SIZE")))
.getFill().setSolidFill(getCellValue_(data, shapeName, "COLOR"));
}
// Shapeのテキストを設定する
function configureShapeText_(shape, data, shapeName) {
const textContent = `${getCellValue_(data, shapeName, "BTN_NAME")}\n${getCellValue_(data, shapeName, "GPIO_PIN")}`;
shape.getText()
.setText(textContent)
.getTextStyle()
.setFontSize(10)
.setForegroundColor("#000000");
}
// Frameのサイズと位置を調整する
function adjustFrameShape_(shape, data) {
shape
.setLeft(0)
.setTop(0)
.setWidth(cmToPoints_(getCellValue_(data, "MAX", "X_POS")) + cmToPoints_(getCellValue_(data, "MAX", "REF_SIZE")))
.setHeight(cmToPoints_(getCellValue_(data, "MAX", "Y_POS")) + cmToPoints_(getCellValue_(data, "MAX", "REF_SIZE")))
.getFill().setTransparent();
}
// Shapeの枠線を赤にする
function highlightShapeBorder_(shape) {
shape.getBorder().getLineFill().setSolidFill("#FF0000");
}
// Config用の文字列を生成する
function generateTempConfig_(shapeName, data) {
const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
const confX = getCellValue_(data, shapeName, "CFG_X");
const confY = getCellValue_(data, shapeName, "CFG_Y");
const size = getCellValue_(data, shapeName, "CFG_SIZE");
const mask = `GAMEPAD_MASK_${shapeName}`;
return ` {GP_ELEMENT_${elementType}_BUTTON, {${confX}, ${confY}, ${size}, ${size}, 1, 1, ${mask}, GP_SHAPE_ELLIPSE}}, \\\n`;
}
// スライド上のすべてのオブジェクトをグループ化する
function groupAllSlideObjects_(slide) {
const pageElements = slide.getPageElements();
if (pageElements.length > 1) {
const group = slide.group(pageElements);
centerObjectOnSlide_(group);
}
}
// オブジェクトをスライドの中央に配置する
function centerObjectOnSlide_(object) {
const presentation = SlidesApp.getActivePresentation();
const slideWidth = presentation.getPageWidth();
const slideHeight = presentation.getPageHeight();
const newX = (slideWidth - object.getWidth()) / 2;
const newY = (slideHeight - object.getHeight()) / 2;
object.setLeft(newX);
object.setTop(newY);
}
// Configをログ出力する
function logConfig_(config) {
Logger.log(config.tmpA);
Logger.log(config.tmpB);
}
// セルの値を取得する
function getCellValue_(data, button, header) {
const headers = data[0];
const rows = data.slice(1);
const columnIndex = headers.indexOf(header);
if (columnIndex === -1) return 'Header not found.';
return button === "MAX" ? getMaxValue_(rows, columnIndex) : getValueByButton_(rows, columnIndex, button);
}
// ヘッダー列の最大値を取得する
function getMaxValue_(rows, columnIndex) {
const numericValues = rows.map(row => parseFloat(row[columnIndex])).filter(val => !isNaN(val));
return numericValues.length > 0 ? Math.max(...numericValues) : 'No numeric values found in column.';
}
// 指定された値を含む行からセルの値を取得する
function getValueByButton_(rows, columnIndex, button) {
const row = rows.find(row => row.includes(button));
return row ? row[columnIndex] : 'Value not found.';
}
// cmをpointに変換する
function cmToPoints_(cm) {
return cm * (72 / 2.54);
}
4.プルダウンをButtonSetupにし、▶実行をクリック
5.スライドに戻ると・・・あら不思議!!
enjoy!!
この記事が気に入ったらサポートをしてみませんか?