Skip to content

Instantly share code, notes, and snippets.

@motsu0
Created September 21, 2023 12:41
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/65ae3596123465925477509b2928ba12 to your computer and use it in GitHub Desktop.
Save motsu0/65ae3596123465925477509b2928ba12 to your computer and use it in GitHub Desktop.
.sample-img {
width: 200px;
}
/* */
.settings {
display: flex;
flex-direction: column;
row-gap: 12px;
align-items: center;
}
.text-input {
width: 100%;
max-width: 300px;
height: 2em;
padding: 0 2px;
}
#submit-bt {
width: 100px;
height: 44px;
cursor: pointer;
}
/* */
.canvas-area {
margin: 12px 0;
text-align: center;
}
.canvas-container {
box-sizing: border-box;
display: inline-flex;
max-width: 90%;
position: relative;
border: 1px solid #ccc;
overflow: hidden;
}
#base-canvas {
width: 100%;
max-height: 80vh;
}
#letter-canvas {
width: 80%;
height: 100%;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
}
/* */
.output {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 12px;
}
.image-type-setting {
display: flex;
flex-direction: column;
row-gap: 8px;
}
.image-type-title {
font-weight: bold;
border-bottom: 1px solid #333;
}
.image-type-setting__row {
display: flex;
column-gap: 12px;
}
#quality-input {
width: 60px;
}
#output-bt {
padding: 4px 8px;
cursor: pointer;
}
<div class="settings">
<div class="settings__row">
<input type="file" id="file-input" />
</div>
<div class="settings__row">
肩書き<br />
<input
type="text"
id="title-input"
class="text-input"
placeholder="肩書き"
/>
</div>
<div class="settings__row">
名前<br />
<input
type="text"
id="name-input"
class="text-input"
placeholder="名前"
/>
</div>
<div class="settngs__row">
<button id="submit-bt" disabled>作成</button>
</div>
</div>
<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>
<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 modal = new simpleImgModal();
const dialog = new simpleDialog({
str_head: 'エラー',
});
const fileInput = document.getElementById('file-input');
const titleInput = document.getElementById('title-input');
const nameInput = document.getElementById('name-input');
const submitBt = document.getElementById('submit-bt');
const baseCanvas = document.getElementById('base-canvas');
const baseCtx = baseCanvas.getContext('2d');
const letterCanvas = document.getElementById('letter-canvas');
const letterCtx = letterCanvas.getContext('2d');
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 fontName = 'aoyagireisyosimo';
const fontPath = '/fonts/aoyagireisyosimo.otf';
const loadFontPromise = loadLocalFont(fontName, fontPath);
fileInput.addEventListener('change', checkFile);
submitBt.addEventListener('click', drawLetter);
[jpgRadio, pngRadio].forEach((el) => {
el.addEventListener('change', () => {
if (!jpgRadio.checked) {
qualityInput.disabled = true;
} else {
qualityInput.disabled = false;
}
});
});
outputBt.addEventListener('click', downloadImage);
nowloading.stop();
async function drawLetter() {
if (baseCanvas.width === '') return;
letterCtx.clearRect(0, 0, letterCanvas.width, letterCanvas.height);
letterCanvas.width = baseCanvas.width;
letterCanvas.height = baseCanvas.height;
await loadFontPromise;
letterCtx.textBaseline = 'middle';
letterCtx.textAlign = 'center';
// 肩書 サイズ
const titleFontSize = Math.round(letterCanvas.width / 16);
const titleY = letterCanvas.height - titleFontSize * 3.4;
letterCtx.font = `${titleFontSize}px '${fontName}'`;
// letterCtx.letterSpacing = `${-Math.round(titleFontSize / 6)}px`;
// 肩書 縁黒
letterCtx.shadowBlur = Math.max(titleFontSize / 5, 2);
letterCtx.shadowColor = '#000000';
letterCtx.lineWidth = Math.max(titleFontSize / 10, 2);
letterCtx.strokeStyle = '#000000';
letterCtx.strokeText(
titleInput.value,
letterCanvas.width / 2,
titleY,
letterCanvas.width
);
letterCtx.shadowBlur = 0;
// 肩書 縁白
letterCtx.lineWidth = Math.max(titleFontSize / 24, 1);
letterCtx.strokeStyle = '#ffffff';
letterCtx.strokeText(
titleInput.value,
letterCanvas.width / 2,
titleY,
letterCanvas.width
);
// 肩書 赤
(letterCtx.fillStyle = 'rgb(180 0 0)'),
letterCtx.fillText(
titleInput.value,
letterCanvas.width / 2,
titleY,
letterCanvas.width
);
// 名前 サイズ
const nameFontSize = Math.round(letterCanvas.width / 6);
const nameY = letterCanvas.height - nameFontSize * 0.6;
letterCtx.font = `${nameFontSize}px '${fontName}', serif`;
letterCtx.letterSpacing = `${Math.round(nameFontSize / 10)}px`;
// 名前 縁黒
letterCtx.shadowBlur = Math.max(nameFontSize / 12, 2);
letterCtx.shadowColor = '#000000';
letterCtx.lineWidth = Math.max(nameFontSize / 20, 2);
letterCtx.strokeStyle = '#000000';
letterCtx.strokeText(
nameInput.value,
letterCanvas.width / 2,
nameY,
letterCanvas.width
);
letterCtx.shadowBlur = 0;
// 名前 縁白
letterCtx.lineWidth = Math.max(nameFontSize / 40, 1);
letterCtx.strokeStyle = '#ffffff';
letterCtx.strokeText(
nameInput.value,
letterCanvas.width / 2,
nameY,
letterCanvas.width
);
// 名前 赤
letterCtx.fillStyle = 'rgb(180 0 0)';
letterCtx.fillText(
nameInput.value,
letterCanvas.width / 2,
nameY,
letterCanvas.width
);
}
function drawBase(img) {
baseCanvas.width = img.naturalWidth;
baseCanvas.height = img.naturalHeight;
baseCtx.drawImage(img, 0, 0);
const imageData = baseCtx.getImageData(
0,
0,
baseCanvas.width,
baseCanvas.height
);
for (let i = 0; i < imageData.data.length; i += 4) {
const red = imageData.data[i];
const green = imageData.data[i + 1];
const blue = imageData.data[i + 2];
const gray = Math.round((red + green + blue) / 3);
imageData.data[i] = gray;
imageData.data[i + 1] = gray;
imageData.data[i + 2] = gray;
}
baseCtx.putImageData(imageData, 0, 0);
//
const radius = {
min: Math.min(baseCanvas.width / 2, baseCanvas.height / 2),
max: Math.max(baseCanvas.width / 2, baseCanvas.height / 2),
};
const gradient = baseCtx.createRadialGradient(
baseCanvas.width / 2,
baseCanvas.height / 2,
radius.min * 0.3,
baseCanvas.width / 2,
baseCanvas.height / 2,
radius.max
);
gradient.addColorStop(0.0, 'rgb(0 0 0 /0)');
gradient.addColorStop(1.0, 'rgb(0 0 0 /.5)');
baseCtx.fillStyle = gradient;
baseCtx.fillRect(0, 0, baseCanvas.width, baseCanvas.height);
//
submitBt.disabled = false;
drawLetter();
}
function checkFile(e) {
nowloading.start();
const files = e.target.files;
if (files.length === 0) {
nowloading.stop();
return;
}
const file = files[0];
if (!file.type.includes('image')) {
dialog.setStrBody('画像を選択してください。');
dialog.display();
nowloading.stop();
return;
}
const reader = new FileReader();
reader.onload = (ev) => {
const img = document.createElement('img');
img.onload = () => {
if (img.naturalWidth > 10000 || img.naturalHeight > 10000) {
dialog.setStrBody('画像サイズが大きすぎます。');
dialog.display();
} else {
drawBase(img);
}
nowloading.stop();
};
img.src = ev.target.result;
};
reader.readAsDataURL(file);
}
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);
const dWidth = letterCanvas.width * 0.8;
const dx = (letterCanvas.width - dWidth) / 2;
outputCtx.drawImage(
letterCanvas,
dx,
0,
dWidth,
letterCanvas.height
);
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 loadLocalFont(fontName, path, weight) {
const options = {};
if (weight !== undefined) options.weight = weight;
const font = new FontFace(fontName, `url(${path})`, options);
await font.load();
document.fonts.add(font);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment