Skip to content

Instantly share code, notes, and snippets.

@motsu0
Last active August 22, 2023 07:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save motsu0/c1532ab4142aea3117375a41684131d1 to your computer and use it in GitHub Desktop.
Save motsu0/c1532ab4142aea3117375a41684131d1 to your computer and use it in GitHub Desktop.
.settings {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 20px;
margin: 12px 0;
}
.settings-table {
border-collapse: collapse;
}
.settings-table__cell {
height: 2em;
border: 1px solid #777;
}
th.settings-table__cell {
padding: 4px 8px;
background-color: #eee;
}
td.settings-table__cell {
width: 150px;
}
#text-input,
#font-size-input,
#opacity-input,
#rotate-input,
#padding-input,
#interval-input {
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 0 4px;
border: none;
}
#color-input {
margin: 0 4px;
vertical-align: middle;
}
#opacity-input {
vertical-align: middle;
}
/* */
.canvas-area {
text-align: center;
}
.canvas-container {
box-sizing: border-box;
display: inline-flex;
position: relative;
border: 1px solid #ccc;
}
#base-canvas {
max-width: 100%;
max-height: 80vh;
}
#letter-canvas {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
}
/* */
.output {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 12px;
}
.image-type-setting {
display: flex;
flex-direction: column;
/* align-items: flex-start; */
row-gap: 8px;
}
.image-type-title {
font-weight: bold;
border-bottom: 1px solid #333;
}
.image-type-setting__row {
display: flex;
column-gap: 12px;
/* flex-direction: column; */
}
#quality-input {
width: 60px;
}
#output-bt {
padding: 4px 8px;
cursor: pointer;
}
<h3>設定</h3>
<div class="settings">
<div class="settings__row">
<input type="file" id="file-input" />
</div>
<div class="settings__row">
<table class="settings-table">
<tr>
<th class="settings-table__cell">文字列</th>
<td class="settings-table__cell">
<input
type="text"
id="text-input"
class="settings-input"
placeholder="表示したい文字列"
value="sample"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell">
大きさ<br />(<span id="font-size-min">4</span>-)
</th>
<td class="settings-table__cell">
<input
type="number"
id="font-size-input"
class="settings-input"
min="4"
value="4"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell"></th>
<td class="settings-table__cell">
<input
type="color"
id="color-input"
class="settings-input"
value="#ffffff"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell">不透明度</th>
<td class="settings-table__cell">
<input
type="number"
id="opacity-input"
class="settings-input"
min="0"
max="100"
value="20"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell">文字間隔</th>
<td class="settings-table__cell">
<input
type="number"
id="padding-input"
class="settings-input"
min="0"
value="0"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell">隔行差分</th>
<td class="settings-table__cell">
<input
type="number"
id="interval-input"
class="settings-input"
value="0"
/>
</td>
</tr>
<tr>
<th class="settings-table__cell">角度</th>
<td class="settings-table__cell">
<input
type="number"
id="rotate-input"
class="settings-input"
min="-360"
max="360"
value="0"
/>
</td>
</tr>
</table>
</div>
</div>
<h3>プレビュー</h3>
<div class="canvas-area">
<div class="canvas-container">
<canvas id="letter-canvas" width="0" height="0"></canvas>
<canvas id="base-canvas" width="0" height="0"></canvas>
</div>
</div>
<h3>出力</h3>
<div class="output">
<div class="image-type-setting">
<div class="image-type-title">出力タイプ</div>
<div class="image-type-setting__row">
<label class="settings-label">
<input
type="radio"
value="jpg"
name="image-type"
id="jpg-radio"
checked
/>
JPG
</label>
<div>
(画質:
<input
type="text"
id="quality-input"
value="90"
min="0"
max="100"
/>)
</div>
</div>
<div class="image-type-setting__row">
<label class="settings-label">
<input
type="radio"
value="png"
id="png-radio"
name="image-type"
/>
PNG
</label>
</div>
</div>
<button id="output-bt">保存する</button>
</div>
const nowloading = new nowLoading();
nowloading.start();
const fileInput = document.getElementById('file-input');
const settingsInputs =
document.getElementsByClassName('settings-input');
const textInput = document.getElementById('text-input');
const fontSizeInput = document.getElementById('font-size-input');
const fontSizeMinEl = document.getElementById('font-size-min');
const colorInput = document.getElementById('color-input');
const opacityInput = document.getElementById('opacity-input');
const paddingInput = document.getElementById('padding-input');
const intervalInput = document.getElementById('interval-input');
const rotateInput = document.getElementById('rotate-input');
const jpgRadio = document.getElementById('jpg-radio');
const pngRadio = document.getElementById('png-radio');
const qualityInput = document.getElementById('quality-input');
const outputBt = document.getElementById('output-bt');
const baseCanvas = document.getElementById('base-canvas');
const baseCtx = baseCanvas.getContext('2d');
const letterCanvas = document.getElementById('letter-canvas');
const letterCtx = letterCanvas.getContext('2d');
const canvasPosition = {
x: 0,
y: 0,
};
const cursorPosition = {
x: undefined,
y: undefined,
};
let isDragging = false;
const dialogOption = {
str_head: 'エラー',
};
const dialog = new simpleDialog(dialogOption);
const modal = new simpleImgModal();
// const canvasFontName = 'Borel';
const canvasFontName = 'Noto Sans JP';
const webFontPromise = loadWebFont({
fontName: canvasFontName,
});
initApp();
function initApp() {
fileInput.addEventListener('change', checkFile);
[...settingsInputs].forEach((el) => {
el.addEventListener('input', (e) => {
if (e.currentTarget.id === 'rotate-input') {
canvasPosition.x = 0;
canvasPosition.y = 0;
}
drawLetter();
});
});
[jpgRadio, pngRadio].forEach((el) => {
el.addEventListener('change', () => {
if (!jpgRadio.checked) {
qualityInput.disabled = true;
} else {
qualityInput.disabled = false;
}
});
});
outputBt.addEventListener('click', downloadImage);
letterCanvas.addEventListener('mousedown', dragStart);
letterCanvas.addEventListener('touchstart', dragStart);
letterCanvas.addEventListener('mousemove', drag);
letterCanvas.addEventListener('touchmove', drag);
letterCanvas.addEventListener('mouseup', dragEnd);
letterCanvas.addEventListener('touchend', dragEnd);
letterCanvas.addEventListener('mouseleave', dragEnd);
letterCanvas.addEventListener('touchleave', dragEnd);
//
nowloading.stop();
}
function checkFile(e) {
nowloading.start();
const files = e.target.files;
if (files.length === 0) {
e.target.value = '';
nowloading.stop();
return;
}
const file = files[0];
if (!file.type.includes('image')) {
dialog.setStrBody('画像を選択してください。');
dialog.display();
e.target.value = '';
nowloading.stop();
return;
}
const reader = new FileReader();
reader.onload = (ev) => {
const img = new Image();
img.onload = () => {
if ((img.naturalWidth > 8000) | (img.naturalHeight > 8000)) {
dialog.setStrBody('画像サイズが大きすぎます。');
dialog.display();
} else {
// 処理
initCanvas(img);
}
e.target.value = '';
nowloading.stop();
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
function initCanvas(img) {
const canvasWidth = img.naturalWidth;
const canvasHeight = img.naturalHeight;
baseCanvas.width = canvasWidth;
baseCanvas.height = canvasHeight;
letterCanvas.width = canvasWidth;
letterCanvas.height = canvasHeight;
baseCtx.clearRect(0, 0, canvasWidth, canvasHeight);
baseCtx.drawImage(img, 0, 0);
const lengthMax = Math.max(letterCanvas.width, letterCanvas.height);
const fontSizeMin = Math.round(lengthMax / 20);
fontSizeInput.value = fontSizeMin;
fontSizeMinEl.textContent = fontSizeMin;
drawLetter();
}
async function drawLetter() {
if (letterCanvas.width === 0 || letterCanvas.height === 0) return;
nowloading.start();
letterCtx.resetTransform();
letterCtx.clearRect(0, 0, letterCanvas.width, letterCanvas.height);
const diagonalLength =
Math.sqrt(letterCanvas.width ** 2 + letterCanvas.height ** 2) *
1.05;
const extraWidth = (diagonalLength - letterCanvas.width) / 2;
const extraHeight = (diagonalLength - letterCanvas.height) / 2;
const lengthMax = Math.max(letterCanvas.width, letterCanvas.height);
const text = textInput.value;
const fontSize = (() => {
const fontSizeMin = Math.round(lengthMax / 20);
let temp = Number(fontSizeInput.value);
if (Number.isNaN(temp)) {
fontSizeInput.value = fontSizeMin;
return fontSizeMin;
}
if (temp < 4) {
return 4;
}
return temp;
})();
const opacity = (() => {
let temp = Number(opacityInput.value);
if (Number.isNaN(temp)) return 1;
if (temp < 0) return 0;
if (temp > 100) return 1;
return temp / 100;
})();
const textPadding = (() => {
let temp = Number(paddingInput.value);
if (Number.isNaN(temp)) return 0;
if (temp < 0) return 0;
return temp;
})();
const intervalDiff = (() => {
let temp = Number(intervalInput.value);
if (Number.isNaN(temp)) return 0;
return temp;
})();
const radian = (() => {
let temp = Number(rotateInput.value);
if (Number.isNaN(temp)) return 0;
return (temp * Math.PI) / 180;
})();
await webFontPromise;
letterCtx.font = `${fontSize}px '${canvasFontName}'`;
letterCtx.fillStyle = colorInput.value;
letterCtx.globalAlpha = opacity;
const measure = letterCtx.measureText(text);
const textWidth = measure.width;
const textHeight =
measure.actualBoundingBoxAscent +
measure.actualBoundingBoxDescent;
const originPoint = {
x: letterCanvas.width / 2,
y: letterCanvas.height / 2,
};
letterCtx.translate(
letterCanvas.width / 2,
letterCanvas.height / 2
);
letterCtx.rotate(radian);
letterCtx.translate(
-letterCanvas.width / 2,
-letterCanvas.height / 2
);
const ctxPosition = {
x:
canvasPosition.x * Math.cos(radian) +
canvasPosition.y * Math.sin(radian),
y:
canvasPosition.y * Math.cos(radian) -
canvasPosition.x * Math.sin(radian),
};
const startY = (() => {
const diff =
ctxPosition.y <= 0
? ctxPosition.y % (textHeight + textPadding)
: (ctxPosition.y % (textHeight + textPadding)) -
(textHeight + textPadding);
return -extraHeight + diff;
})();
let rowNumber = 0;
const intervalSub = (() => {
const temp = ctxPosition.y / (textHeight + textPadding);
if (ctxPosition.y > 0) {
return Math.floor(temp) % 2;
}
return 1 - (Math.abs(Math.ceil(temp)) % 2);
})();
for (
let r = startY;
r <= diagonalLength;
r += textPadding + textHeight
) {
const startX = (() => {
const positionX =
rowNumber % 2 === intervalSub
? ctxPosition.x
: ctxPosition.x + intervalDiff;
const diff =
positionX <= 0
? positionX % (textWidth + textPadding)
: (positionX % (textWidth + textPadding)) -
(textWidth + textPadding);
return -extraWidth + diff;
})();
for (
let c = startX;
c <= diagonalLength;
c += textPadding + textWidth
) {
letterCtx.fillText(text, c, r);
}
rowNumber++;
}
nowloading.stop();
}
function dragStart(e) {
isDragging = true;
const event = (() => {
if (e.type === 'mousedown') {
return e;
} else {
return e.changedTouches[0];
}
})();
cursorPosition.x = event.pageX;
cursorPosition.y = event.pageY;
}
function drag(e) {
e.preventDefault();
if (!isDragging) return;
const event = (() => {
if (e.type === 'mousemove') {
return e;
} else {
return e.changedTouches[0];
}
})();
canvasPosition.x += event.pageX - cursorPosition.x;
canvasPosition.y += event.pageY - cursorPosition.y;
cursorPosition.x = event.pageX;
cursorPosition.y = event.pageY;
drawLetter();
}
function dragEnd() {
isDragging = false;
}
function downloadImage() {
nowloading.start();
if (baseCanvas.width === 0 || baseCanvas.height === 0) {
dialog.setStrBody('画像が存在しません。');
dialog.display();
nowloading.stop();
return;
}
const outputCanvas = document.createElement('canvas');
outputCanvas.width = baseCanvas.width;
outputCanvas.height = baseCanvas.height;
const outputCtx = outputCanvas.getContext('2d');
outputCtx.drawImage(baseCanvas, 0, 0);
outputCtx.drawImage(letterCanvas, 0, 0);
const imageType = jpgRadio.checked ? 'jpg' : 'png';
const alink = document.createElement('a');
const quality = (() => {
let temp = Number(qualityInput.value);
if (Number.isNaN(temp)) return 1;
if (temp < 0) return 0;
if (temp > 100) return 1;
return temp / 100;
})();
if (imageType === 'jpg') {
outputCanvas.toBlob(
(blob) => {
alink.href = window.URL.createObjectURL(blob);
alink.download = 'output.jpg';
alink.click();
URL.revokeObjectURL(blob);
nowloading.stop();
},
'image/jpeg',
quality
);
} else {
outputCanvas.toBlob((blob) => {
alink.href = window.URL.createObjectURL(blob);
alink.download = 'output.png';
alink.click();
URL.revokeObjectURL(blob);
nowloading.stop();
}, 'image/png');
}
}
async function loadWebFont(props) {
const { fontName } = props;
const fontNamePlus = fontName.replace(/ /g, '+');
const requestUrl = `https://fonts.googleapis.com/css2?family=${fontNamePlus}`;
try {
const response = await fetch(requestUrl);
if (!response.ok) throw new Error('response error');
const fontFaceCss = await response.text();
const match = fontFaceCss.match(/url\(.*?\)/g);
if (match === null) {
console.log('no fonts');
return;
}
await Promise.all(
match.map(async (url) => {
const font = new FontFace(fontName, url);
await font.load();
document.fonts.add(font);
})
);
return true;
} catch (e) {
console.error(e);
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment