Initial commit

This commit is contained in:
Yuri Sizov
2023-06-30 19:22:13 +02:00
commit fbaa41ada4
8 changed files with 397 additions and 0 deletions

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Python server",
"type": "python",
"request": "launch",
"module": "http.server",
"cwd": "${workspaceFolder}",
"args": [ "8080" ]
}
]
}

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
# MIT License
Copyright © 2023-present Godot Engine contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

9
README.md Normal file
View File

@@ -0,0 +1,9 @@
# Godot Blog Cover Generator
A serverless cover generator for the Godot blog. Uses HTML canvas for rendering, so results might slightly differ between browsers.
Currently relies on `letterSpacing` being available for the canvas 2D context, which doesn't work in Firefox or Safari (see [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/letterSpacing)).
## License
This project is provided under the [MIT License](LICENSE.md).

23
assets/godot-logo.svg Normal file
View File

@@ -0,0 +1,23 @@
<svg
class="godot-logo"
width="792"
height="286"
viewBox="0 0 792 286"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M381.463 109.001C369.107 109.001 359.445 112.913 352.468 120.737C345.485 128.564 342 138.724 342 151.231C342 165.916 345.567 177.049 352.701 184.632C359.848 192.213 369.232 196 380.865 196C387.121 196 392.674 195.458 397.525 194.368C402.382 193.282 405.61 192.168 407.217 191.04L407.698 153.175C407.698 150.974 401.923 149.974 395.446 149.863C388.968 149.762 381.555 151.12 381.555 151.12V163.835H389.01L388.927 169.503C388.927 171.603 386.857 172.657 382.73 172.657C378.597 172.657 374.946 170.898 371.779 167.388C368.606 163.873 367.027 158.735 367.027 151.966C367.027 145.183 368.569 140.183 371.658 136.955C374.74 133.731 378.781 132.114 383.75 132.114C385.838 132.114 387.999 132.452 390.247 133.143C392.496 133.828 393.997 134.47 394.763 135.072C395.523 135.69 396.246 135.984 396.929 135.984C397.609 135.984 398.711 135.183 400.233 133.565C401.759 131.952 403.125 129.508 404.33 126.249C405.53 122.972 406.131 120.461 406.131 118.679C406.131 116.912 406.092 115.694 406.014 115.051C404.329 113.197 401.219 111.726 396.683 110.633C392.156 109.543 387.08 109.002 381.465 109.002L381.463 109.001ZM455.285 109.001C443.933 109.001 434.666 112.672 427.488 120.007C420.311 127.352 416.723 138.139 416.723 152.384C416.723 166.622 420.271 177.452 427.363 184.874C434.466 192.29 443.653 195.998 454.922 195.998C466.195 195.998 475.395 192.433 482.537 185.301C489.68 178.155 493.248 167.264 493.248 152.621C493.248 137.99 489.761 127.051 482.775 119.834C475.802 112.604 466.635 109 455.285 109V109.001ZM614.868 109.001C603.513 109.001 594.252 112.672 587.067 120.007C579.891 127.352 576.303 138.139 576.303 152.384C576.303 166.622 579.852 177.452 586.954 184.874C594.049 192.29 603.231 195.998 614.504 195.998C625.772 195.998 634.979 192.433 642.119 185.301C649.261 178.155 652.83 167.264 652.83 152.621C652.83 137.99 649.343 127.051 642.362 119.834C635.38 112.604 626.22 109 614.868 109V109.001ZM509.287 110.084C507.677 110.084 506.376 110.674 505.375 111.837C504.368 113.018 503.869 114.61 503.869 116.629V189.226C503.869 192.94 505.919 194.791 510.007 194.791H530.578C555.767 194.791 568.366 179.95 568.366 150.262C568.366 136.384 565.054 126.225 558.431 119.772C551.819 113.318 542.133 110.083 529.381 110.083L509.287 110.084ZM659.41 110.084C658.045 110.084 657.082 111.948 656.522 115.658C656.281 117.432 656.166 119.252 656.166 121.1C656.166 122.958 656.281 124.77 656.522 126.541C657.082 130.261 658.045 132.113 659.41 132.113H673.846V192.863C673.846 194.467 677.823 195.28 685.761 195.28C693.707 195.28 697.674 194.467 697.674 192.863V132.113H711.752C713.115 132.113 714.074 130.261 714.643 126.541C714.878 124.77 715 122.958 715 121.1C715 119.252 714.878 117.432 714.643 115.658C714.074 111.948 713.115 110.084 711.752 110.084H659.41ZM454.974 132.235C458.828 132.235 462.057 133.968 464.661 137.432C467.267 140.9 468.575 145.867 468.575 152.319C468.575 158.777 467.309 163.737 464.782 167.202C462.255 170.68 458.964 172.413 454.915 172.413C450.862 172.413 447.597 170.7 445.11 167.263C442.619 163.837 441.376 158.92 441.376 152.497C441.376 146.087 442.684 141.106 445.285 137.558C447.901 134.012 451.125 132.234 454.974 132.234L454.974 132.235ZM614.558 132.235C618.411 132.235 621.636 133.968 624.244 137.432C626.851 140.9 628.156 145.867 628.156 152.319C628.156 158.777 626.891 163.737 624.363 167.202C621.839 170.68 618.546 172.413 614.495 172.413C610.443 172.413 607.174 170.7 604.691 167.263C602.208 163.837 600.961 158.92 600.961 152.497C600.961 146.087 602.263 141.106 604.864 137.558C607.475 134.012 610.708 132.234 614.558 132.234L614.558 132.235ZM526.965 133.073H528.168C533.302 133.073 537.054 134.254 539.423 136.594C541.78 138.927 542.966 143.95 542.966 151.659C542.966 159.364 541.833 164.865 539.54 168.169C537.253 171.476 533.908 173.135 529.492 173.135C528.292 173.135 527.568 172.913 527.329 172.468C527.088 172.021 526.964 170.956 526.964 169.265L526.965 133.073Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M142.896 40.3251C152.949 35.2648 163.673 31.9105 174.513 29.5006L174.513 29.5004C178.85 36.7781 182.807 44.6586 186.253 52.3627C190.343 51.6793 194.449 51.4259 198.57 51.377V51.3715C198.596 51.3715 198.624 51.377 198.649 51.377C198.661 51.377 198.673 51.3757 198.686 51.3744C198.7 51.373 198.714 51.3715 198.729 51.3715V51.377C202.843 51.426 206.951 51.6793 211.041 52.3627C214.489 44.6579 218.449 36.7781 222.779 29.5004C233.625 31.9101 244.348 35.2646 254.402 40.3249C254.171 49.202 253.597 57.7079 252.436 66.3479C253.654 67.1296 254.893 67.8767 256.132 68.6239C258.856 70.2673 261.582 71.9111 264.088 73.9242C264.673 74.3735 265.26 74.8198 265.846 75.2659C268.979 77.6499 272.105 80.0283 274.93 82.8366C281.633 78.4026 288.73 74.2365 296.036 70.5588C303.913 79.0365 311.284 88.1875 317.299 98.4276C312.769 105.75 308.046 112.615 302.942 119.129H302.8V176.955L302.784 176.96V181.975C302.669 181.976 302.554 181.981 302.44 181.992L263.928 185.706C261.91 185.902 260.329 187.525 260.188 189.548L259.001 206.555L225.392 208.954L223.077 193.256C222.777 191.221 221.031 189.714 218.973 189.714H178.325C176.266 189.714 174.52 191.221 174.22 193.256L171.905 208.954L138.297 206.555L137.109 189.548C136.968 187.525 135.387 185.901 133.369 185.706L94.8408 181.992C94.7269 181.982 94.6111 181.976 94.4961 181.975V119.13H94.3541C89.251 112.616 84.5247 105.751 79.9977 98.4278C86.0148 88.1884 93.3815 79.0374 101.259 70.559C108.567 74.2371 115.661 78.4035 122.363 82.8368C125.168 80.0498 128.273 77.6861 131.381 75.3203C131.991 74.8561 132.601 74.3919 133.208 73.9244C135.725 71.9041 138.459 70.2556 141.194 68.6059C142.423 67.8648 143.653 67.1235 144.863 66.3481C143.699 57.7081 143.126 49.2022 142.896 40.3251ZM94.4765 196.643C94.4612 194.932 94.44 192.544 94.4303 190.287L94.4313 190.287L129.076 193.627L130.268 210.726C130.412 212.787 132.051 214.427 134.112 214.575L175.152 217.503C177.315 217.661 179.235 216.116 179.551 213.968L181.904 198.009H215.39L217.744 213.968C218.045 216.016 219.804 217.514 221.844 217.514C221.943 217.514 222.043 217.51 222.143 217.503L263.183 214.575C265.243 214.427 266.882 212.787 267.026 210.726L268.22 193.627L302.851 190.287C302.84 192.539 302.818 194.913 302.801 196.622C302.791 197.741 302.783 198.574 302.783 198.902C302.783 235.49 256.383 253.077 198.716 253.279H198.574C140.909 253.076 94.4937 235.49 94.4937 198.902C94.4937 198.582 94.4863 197.755 94.4765 196.643ZM120.815 144.641C120.815 131.821 131.213 121.434 144.031 121.434C156.855 121.434 167.249 131.821 167.249 144.641C167.249 157.469 156.855 167.861 144.031 167.861C131.212 167.861 120.815 157.469 120.815 144.641ZM230.044 144.641C230.044 131.821 240.438 121.434 253.264 121.434C266.081 121.434 276.476 131.821 276.476 144.641C276.476 157.469 266.081 167.861 253.264 167.861C240.438 167.861 230.044 157.469 230.044 144.641ZM191.169 141.806C191.169 138.059 194.514 135.014 198.642 135.014C202.77 135.014 206.124 138.059 206.124 141.806V163.181C206.124 166.931 202.77 169.973 198.642 169.973C194.514 169.973 191.169 166.931 191.169 163.181V141.806Z"
fill="white"
/>
<path
d="M146.252 130.61C137.74 130.61 130.841 137.513 130.841 146.022C130.841 154.531 137.741 161.427 146.252 161.427C154.768 161.427 161.664 154.531 161.664 146.022C161.664 137.513 154.767 130.61 146.252 130.61ZM251.038 130.61C242.529 130.61 235.635 137.513 235.635 146.022C235.635 154.531 242.529 161.427 251.038 161.427C259.556 161.427 266.449 154.531 266.449 146.022C266.449 137.513 259.556 130.61 251.038 130.61V130.61Z"
fill="white"
/>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
assets/output-vercel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 887 KiB

55
index.html Normal file
View File

@@ -0,0 +1,55 @@
<html>
<head>
<title>Article cover generator - Godot Engine</title>
<link rel="stylesheet" href="style.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;500;800&display=swap"
rel="stylesheet"
/>
</head>
<body>
<script src="script.js"></script>
<div class="content">
<div class="toolbar">
<h3>Configuration</h3>
<div class="toolbar-item">
<label for="background-image">Featured image</label>
<input type="file" accept="image/*" id="background-image" />
</div>
<div class="toolbar-item">
<label for="title-text">Title text</label>
<input type="text" id="title-text" value="" />
</div>
<div class="toolbar-item">
<label for="super-text">Supertext</label>
<input type="text" id="super-text" value="" />
</div>
<div class="toolbar-item">
<button id="download-image">Download</button>
</div>
<h3>Debug options</h3>
<div class="toolbar-item">
<label for="show-vercel">Show Vercel output</label>
<input type="checkbox" id="show-vercel" />
</div>
</div>
<div class="canvas-container">
<canvas id="generator"></canvas>
<canvas id="render-target" style="display:none;"></canvas>
</div>
</div>
</body>
</html>

242
script.js Normal file
View File

@@ -0,0 +1,242 @@
document.addEventListener("DOMContentLoaded", () => {
const generator = new PreviewGenerator();
generator.init();
});
class PreviewGenerator {
constructor() {
/**
* @type HTMLCanvasElement
*/
this.previewCanvas = null;
/**
* @type HTMLCanvasElement
*/
this.targetCanvas = null;
/**
* @type CanvasRenderingContext2D
*/
this.ctx = null;
this.targetWidth = 1280;
this.targetHeight = 720;
this.previewScale = 2.0;
this.previewWidth = this.targetWidth * this.previewScale;
this.previewHeight = this.targetHeight * this.previewScale;
/**
* @type Image
*/
this.godotLogo = null;
// Dynamic parameters.
/**
* @type Image
*/
this.coverImage = null;
this.titleText = "";
this.superText = "";
// TODO: Remove when matched.
this.vercelOutput = null;
this.showVercel = false;
}
init() {
this.targetCanvas = document.getElementById("render-target");
this.targetCanvas.width = this.targetWidth;
this.targetCanvas.height = this.targetHeight;
this.previewCanvas = document.getElementById("generator");
this.previewCanvas.width = this.previewWidth;
this.previewCanvas.height = this.previewHeight;
this.ctx = this.previewCanvas.getContext("2d");
// Fonts may take a moment to load, make sure to update preview when
// they are done loading.
document.fonts.onloadingdone = () => {
this.render();
}
// Load the Godot logo.
this._loadImage("assets/godot-logo.svg", (image) => {
this.godotLogo = image;
this.render();
});
// Connect to the toolbar panel to react on changes.
const backgroundImage_selector = document.getElementById("background-image");
backgroundImage_selector.addEventListener("change", () => {
const selectedFiles = backgroundImage_selector.files;
if (selectedFiles.length === 0) {
this.coverImage = null;
this.render();
} else {
const imageFile = selectedFiles[0];
createImageBitmap(imageFile)
.then((res) => {
this.coverImage = res;
this.render();
})
.catch((err) => {
this.coverImage = null;
this.render();
});
}
});
const titleText_input = document.getElementById("title-text");
titleText_input.addEventListener("change", () => {
this.titleText = titleText_input.value;
this.render();
});
const superText_input = document.getElementById("super-text");
superText_input.addEventListener("change", () => {
this.superText = superText_input.value;
this.render();
});
const downloadImage_button = document.getElementById("download-image");
downloadImage_button.addEventListener("click", () => {
this._saveRender();
});
// TODO: REMOVE
const showVercel_toggle = document.getElementById("show-vercel");
showVercel_toggle.addEventListener("change", () => {
this.showVercel = showVercel_toggle.checked;
this.render();
});
// Do the first render.
this.render();
}
testVercel() {
this._loadImage("assets/output-vercel.png", (image) => {
this.vercelOutput = image;
this.render();
});
}
testCover() {
this._loadImage("assets/test-cover.webp", (image) => {
this.coverImage = image;
this.render();
});
}
render() {
if (!this.previewCanvas || !this.ctx) {
return;
}
// Clear the canvas.
this.ctx.clearRect(0, 0, this.previewCanvas.width, this.previewCanvas.height);
// Reset rendering styles.
this.ctx.fillStyle = "black";
this.ctx.shadowBlur = 0;
this.ctx.shadowColor = "rgba(0, 0, 0, 0)";
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.font = "10px sans-serif";
this.ctx.letterSpacing = "0px";
// Render the cover image.
if (this.coverImage) {
this.ctx.drawImage(this.coverImage, 0, 0, this.previewWidth, this.previewHeight);
}
// Render the overlay as a gradient from top-right to bottom-left.
const overlayGradient = this.ctx.createLinearGradient(this.previewWidth, 0, 0, this.previewHeight);
overlayGradient.addColorStop(0, "rgba(21, 79, 159, 0.1)");
overlayGradient.addColorStop(0.85, "rgba(9, 13, 30, 0.4)");
this.ctx.fillStyle = overlayGradient;
this.ctx.fillRect(0, 0, this.previewWidth, this.previewHeight);
// Render decorations.
const relativeUnit = (this.previewWidth - 6) / 100.0;
const paddingSize = 4 * relativeUnit;
// Render the title.
const titleSize = 8 * relativeUnit;
const titleOffset = 2 * relativeUnit + paddingSize + 0.2 * titleSize;
this.ctx.font = `bold ${titleSize}px 'JetBrains Mono', monospace`;
this.ctx.letterSpacing = "0px";
this.ctx.fillStyle = "white";
this.ctx.fillText(this.titleText, paddingSize, this.previewHeight - titleOffset);
// Render the super text.
const supertextSize = 3.5 * relativeUnit;
const supertextOffset = 3 * relativeUnit + titleSize + titleOffset + 0.06 * supertextSize;
this.ctx.font = `bold ${supertextSize}px 'JetBrains Mono', monospace`;
this.ctx.letterSpacing = `${1.4 * relativeUnit}px`;
this.ctx.fillStyle = "white";
this.ctx.fillText(this.superText.toUpperCase(), paddingSize, this.previewHeight - supertextOffset);
// Render break line.
const breaklineWidth = 8 * relativeUnit;
const breaklineHeight = 0.6 * relativeUnit;
const breaklineOffset = 3 * relativeUnit + supertextSize + supertextOffset + 0.2 * breaklineHeight;
this.ctx.fillStyle = "white";
this.ctx.fillRect(paddingSize, this.previewHeight - breaklineOffset - breaklineHeight, breaklineWidth, breaklineHeight);
// Render the Godot logo.
if (this.godotLogo) {
const logoWidth = 0.36 * this.previewWidth;
const logoHeight = this.godotLogo.height * (logoWidth / this.godotLogo.width);
this.ctx.shadowBlur = 60;
this.ctx.shadowColor = "rgb(0 0 0 / 0.3)";
this.ctx.shadowOffsetX = 0;
this.ctx.shadowOffsetY = 0;
this.ctx.drawImage(this.godotLogo, this.previewWidth - paddingSize - logoWidth, paddingSize, logoWidth, logoHeight);
}
// TODO: REMOVE
if (this.showVercel && this.vercelOutput) {
this.ctx.drawImage(this.vercelOutput, 0, 0, this.previewWidth, this.previewHeight);
}
}
/**
*
* @param {String} imagePath
* @param {CallableFunction} callback
*/
_loadImage(imagePath, callback) {
const image = new Image();
image.onload = () => {
callback(image);
};
image.src = imagePath;
}
_saveRender() {
if (!this.previewCanvas || !this.targetCanvas) {
return;
}
const targetContext = this.targetCanvas.getContext("2d");
targetContext.clearRect(0, 0, this.targetCanvas.width, this.targetCanvas.height);
targetContext.drawImage(this.previewCanvas, 0, 0, this.targetWidth, this.targetHeight);
const imageData = this.targetCanvas.toDataURL("image/webp", 0.95);
const fakeAnchor = document.createElement("A");
fakeAnchor.setAttribute("download", "image.webp");
fakeAnchor.setAttribute("href", imageData);
fakeAnchor.click();
}
}

31
style.css Normal file
View File

@@ -0,0 +1,31 @@
/* GENERAL LAYOUT. */
.content {
display: flex;
flex-direction: row;
gap: 24px;
}
/* TOOLBAR STYLING. */
.toolbar {
display: flex;
flex-direction: column;
gap: 12px;
}
.toolbar-item {
}
/* CANVAS STYLING. */
.canvas-container {
max-height: 540px;
aspect-ratio: 16 / 9;
}
.canvas-container > canvas {
width: 100%;
height: 100%;
}