Last active
August 22, 2023 07:25
-
-
Save motsu0/c1532ab4142aea3117375a41684131d1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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