og-image/src/page/OgImageGeneratorPage.vue
Simon Vieille ac18fbdeef
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
update default value of the author size
2024-04-18 20:25:20 +02:00

754 lines
20 KiB
Vue

<template>
<div class="flex">
<div
class="lg:mr-4 tools"
ref="tools"
>
<div class="mb-2 flex flex-row">
<button
v-for="(name, value) in tools"
class="p-3 mr-2 rounded"
:class="{'bg-slate-300': this.tool == value}"
@click="this.tool = value"
v-text="name"
></button>
</div>
<div
:class="{hidden: this.tool !== 'heading'}"
class="drop-shadow-md rounded-md md:border border-zinc-300 tool"
>
<div class="p-5">
<div>
<TextareaForm
id="heading"
v-model="heading"
label="Heading"
/>
<RangeForm
id="heading-size"
v-model="headingSize"
min="1"
max="20"
step="0.1"
/>
</div>
<div class="py-6">
<TextareaForm
id="subheading"
v-model="subheading"
label="Subheading"
/>
<RangeForm
id="subheading-size"
v-model="subheadingSize"
min="1"
max="20"
step="0.1"
/>
</div>
<div class="pb-6">
<CheckboxForm
id="allow-html"
v-model="allowHtml"
label="Allow HTML"
/>
</div>
<div class="pb-6">
<RangeForm
id="heading-spacing"
v-model="headingSpacing"
min="0"
max="300"
step="0.05"
label="Spacing"
/>
</div>
<div class="pb-6">
<RangeForm
id="content-padding"
v-model="contentPadding"
min="0"
max="300"
step="0.05"
label="Padding"
/>
</div>
<div class="pb-3">
<InputForm
id="text-color"
v-model="textColor"
label="Text color"
type="color"
/>
</div>
<div>
<div class="text-center stroke-gray-400">
<AlignForm v-model="headingAlign" />
</div>
</div>
</div>
</div>
<div
:class="{hidden: this.tool !== 'author'}"
class="drop-shadow-md rounded-md md:border border-zinc-300 tool"
>
<div class="p-5">
<CheckboxForm
id="author-show"
v-model="authorShow"
label="Show the block"
/>
</div>
<div class="p-5 pt-0">
<div>
<InputForm
id="author"
v-model="author"
label="Author"
/>
<RangeForm
id="author-size"
v-model="authorSize"
min="1"
max="20"
step="0.1"
/>
</div>
<div class="py-6">
<FileForm
id="avatar"
v-model="avatar"
label="Avatar"
/>
</div>
<div>
<FileForm
id="logo"
v-model="logo"
label="Logo"
/>
</div>
<div class="stroke-gray-400 py-6">
<RangeForm
id="author-margin-bottom"
v-model="authorMarginBottom"
min="0"
max="500"
step="0.1"
label="Position"
/>
<RangeForm
id="author-padding"
v-model="authorPadding"
min="0"
max="50"
step="0.05"
label="Padding"
/>
</div>
<div class="pb-3">
<InputForm
id="author-text-color"
v-model="authorTextColor"
label="Text color"
type="color"
/>
</div>
<div>
<InputForm
id="background-color"
v-model="authorBackgroundColor"
label="Background color"
type="color"
/>
<RangeForm
id="author-background-opacity"
v-model="authorBackgroundOpacity"
min="0"
max="1"
step="0.05"
/>
</div>
<div>
<div class="text-center stroke-gray-400 pt-5">
<AlignForm v-model="authorAlign" />
</div>
</div>
</div>
</div>
<div
:class="{hidden: this.tool !== 'font'}"
class="drop-shadow-md rounded-md md:border border-zinc-300 tool"
>
<div class="p-5">
<SelectForm
id="font"
v-model="font"
:items="fonts"
label="Font"
/>
</div>
</div>
<div
:class="{hidden: this.tool !== 'background'}"
class="drop-shadow-md rounded-md md:border border-zinc-300 tool"
>
<div class="p-5">
<div class="pb-6">
<FileForm
id="background"
v-model="background"
label="Background"
/>
</div>
<div>
<InputForm
id="background-hover"
v-model="backgroundHover"
label="Background hover"
type="color"
/>
<RangeForm
id="background-hover-opacity"
v-model="backgroundHoverOpacity"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
</div>
<div
:class="{hidden: this.tool !== 'sizes'}"
class="drop-shadow-md rounded-md md:border border-zinc-300 tool"
>
<div class="p-5">
<div>
<label
class="block text-sm font-medium text-gray-700"
for="width"
>Sizes</label>
<div class="grid grid-cols-2 gap-x-4">
<InputForm
id="width"
v-model="width"
type="number"
/>
<InputForm
id="height"
v-model="height"
type="number"
/>
</div>
<h3 class="mt-4 block text-sm font-medium text-gray-700">Recommendations</h3>
<button
v-for="(sizes, name) in sizesRecommendations"
class="bg-lime-300 rounded px-3 py-2 mt-1 mr-1"
@click="this.setSizes(sizes[0], sizes[1])"
v-text="name"
></button>
</div>
</div>
</div>
</div>
<div
ref="ogwrapper"
class="og-wrapper pl-10"
>
<div class="og-wrapper-content">
<svg
:viewBox="[0, 0, width, height].join(' ')"
:style="ogSvgStyle()"
:width="ogMaxWidth"
:height="ogMaxHeight"
>
<foreignObject
:style="ogSvgFoStyle()"
>
<div
ref="og"
class="og"
:style="ogStyle()"
>
<div
ref="render"
class="og-image"
:style="ogImageStyle()"
>
<div
class="og-image-hover"
:style="ogImageHoverStyle()"
>
<div
class="h-full flex flex-col justify-between"
:style="ogContentStyle()"
>
<div v-if="allowHtml">
<h1
class="leading-none"
:style="ogHeadingStyle()"
v-html="heading"
/>
<p
class="leading-tight"
:style="ogSubheadingStyle()"
v-html="subheading"
/>
</div>
<div v-else>
<h1
class="leading-none"
:style="ogHeadingStyle()"
v-text="heading"
/>
<p
class="leading-tight"
:style="ogSubheadingStyle()"
v-text="subheading"
/>
</div>
<div
class="w-full flex flex-row items-center og-image--footer"
:style="ogAuthorStyle()"
>
<img
v-if="avatar"
class="rounded-full"
:style="ogAuthorImageStyle('avatar')"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
>
<span
class="mx-4"
:style="ogAuthorTextStyle()"
v-text="author"
/>
<img
v-if="logo"
class="rounded-full"
:style="ogAuthorImageStyle('logo')"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
>
</div>
</div>
</div>
</div>
</div>
</foreignObject>
</svg>
</div>
<div class="text-center mt-3">
<DownloadButton
class="rounded-l-md"
label="PNG"
@click="downloadAsPng"
/>
<DownloadButton
class="rounded-r-md"
label="JPEG"
@click="downloadAsJpeg"
/>
</div>
</div>
</div>
</template>
<script>
import { hexToRgb } from '../util/color'
import { toPng, toJpeg } from 'html-to-image'
import InputForm from '../ui/InputForm'
import RangeForm from '../ui/RangeForm'
import TextareaForm from '../ui/TextareaForm'
import FileForm from '../ui/FileForm'
import SelectForm from '../ui/SelectForm'
import CheckboxForm from '../ui/CheckboxForm'
import AlignForm from '../ui/AlignForm'
import DownloadButton from '../ui/DownloadButton'
export default {
components: {
InputForm,
RangeForm,
TextareaForm,
FileForm,
SelectForm,
CheckboxForm,
AlignForm,
DownloadButton,
},
data() {
return {
tool: localStorage.getItem('tool') ?? 'heading',
author: localStorage.getItem('author') ?? 'Author name',
authorSize: localStorage.getItem('authorSize') ?? 1.5,
authorShow: localStorage.getItem('authorShow') ?? 1,
authorPadding: localStorage.getItem('authorPadding') ?? 10,
authorAlign: localStorage.getItem('authorAlign') ?? 'left',
authorTextColor: localStorage.getItem('authorTextColor') ?? '#fff9e6',
authorMarginBottom: localStorage.getItem('authorMarginBottom') ?? 200,
authorBackgroundColor: localStorage.getItem('authorBackgroundColor') ?? '#fc5705',
authorBackgroundOpacity: localStorage.getItem('authorBackgroundOpacity') ?? 1,
heading: localStorage.getItem('heading') ?? 'Heading',
subheading: localStorage.getItem('subheading') ?? 'Subheading',
headingSpacing: localStorage.getItem('headingSpacing') ?? 10,
headingAlign: localStorage.getItem('headingAlign') ?? 'left',
font: localStorage.getItem('font') ?? 'Trebuchet MS',
avatar: localStorage.getItem('avatar'),
logo: localStorage.getItem('logo'),
textColor: localStorage.getItem('textColor') ?? '#fc5705',
background: localStorage.getItem('background'),
backgroundHover: localStorage.getItem('backgroundHover') ?? '#fff9e6',
backgroundHoverOpacity: localStorage.getItem('backgroundHoverOpacity') ?? 1,
headingSize: localStorage.getItem('headingSize') ?? 4,
subheadingSize: localStorage.getItem('subheadingSize') ?? 2,
width: localStorage.getItem('width') ?? 1200,
height: localStorage.getItem('height') ?? 600,
contentPadding: localStorage.getItem('contentPadding') ?? 20,
allowHtml: localStorage.getItem('allowHtml') ?? 0,
ogMaxWidth: 100,
tools: {
heading: 'Heading',
author: 'Author',
font: 'Font',
background: 'Background',
sizes: 'Sizes',
},
sizesRecommendations: {
Mastodon: [1200, 600],
LinkedIn: [1200, 627],
Instagram: [1200, 630],
X: [1200, 675],
},
fonts: [
{label: 'Arial (sans-serif)', value : 'Arial'},
{label: 'Brush Script MT (cursive)', value : 'Brush Script MT'},
{label: 'Calibri (sans-serif)', value : 'Calibri'},
{label: 'Courier New (monospace)', value : 'Courier New'},
{label: 'Garamond (serif)', value : 'Garamond'},
{label: 'Georgia (serif)', value : 'Georgia'},
{label: 'Tahoma (sans-serif)', value : 'Tahoma'},
{label: 'Times New Roman (serif)', value : 'Times New Roman'},
{label: 'Trebuchet MS (sans-serif)', value : 'Trebuchet MS'},
{label: 'Verdana (sans-serif)', value : 'Verdana'},
],
}
},
watch: {
tool(value) {
localStorage.setItem('tool', value)
},
author(value) {
localStorage.setItem('author', value)
},
authorShow(value) {
localStorage.setItem('authorShow', value)
},
authorSize(value) {
localStorage.setItem('authorSize', value)
},
authorAlign(value) {
localStorage.setItem('authorAlign', value)
},
authorPadding(value) {
localStorage.setItem('authorPadding', value)
},
authorBackgroundColor(value) {
localStorage.setItem('authorBackgroundColor', value)
},
authorBackgroundOpacity(value) {
localStorage.setItem('authorBackgroundOpacity', value)
},
authorTextColor(value) {
localStorage.setItem('authorTextColor', value)
},
authorMarginBottom(value) {
localStorage.setItem('authorMarginBottom', value)
},
heading(value) {
localStorage.setItem('heading', value)
},
subheading(value) {
localStorage.setItem('subheading', value)
},
headingSpacing(value) {
localStorage.setItem('headingSpacing', value)
},
avatar(value) {
localStorage.setItem('avatar', value)
},
logo(value) {
localStorage.setItem('logo', value)
},
textColor(value) {
localStorage.setItem('textColor', value)
},
background(value) {
localStorage.setItem('background', value)
},
backgroundHover(value) {
localStorage.setItem('backgroundHover', value)
},
backgroundHoverOpacity(value) {
localStorage.setItem('backgroundHoverOpacity', value)
},
headingSize(value) {
localStorage.setItem('headingSize', value)
},
subheadingSize(value) {
localStorage.setItem('subheadingSize', value)
},
headingAlign(value) {
localStorage.setItem('headingAlign', value)
},
width(value) {
localStorage.setItem('width', value)
},
height(value) {
localStorage.setItem('height', value)
},
contentPadding(value) {
localStorage.setItem('contentPadding', value)
},
font(value) {
localStorage.setItem('font', value)
},
allowHtml(value) {
localStorage.setItem('allowHtml', value)
},
},
mounted() {
this.updateOgMaxWidth()
window.addEventListener('resize', this.updateOgMaxWidth, false)
},
methods: {
ogSvgStyle() {
return {
aspectRatio: `${this.width}/${this.height}`,
maxHeight: `${this.ogMaxHeight}px !important`,
maxWidth: '100%',
}
},
ogSvgFoStyle() {
return {
aspectRatio: `${this.width}/${this.height}`,
maxHeight: `${this.ogMaxHeight}px !important`,
width: `${this.width}px`,
height: `${this.height}px`,
}
},
ogStyle() {
return {
color: this.textColor,
aspectRatio: `${this.width}/${this.height}`,
maxHeight: `${this.ogMaxHeight}px !important`,
width: `${this.width}px`,
height: `${this.height}px`,
}
},
ogImageStyle() {
let value = {
aspectRatio: `${this.width}/${this.height}`
}
if (this.background) {
value.background = `url(${this.background}) center`
value.backgroundSize = 'cover'
}
return value
},
ogHeadingStyle() {
return {
fontSize: `${this.headingSize}em`,
textAlign: this.headingAlign,
}
},
ogSubheadingStyle() {
return {
fontSize: `${this.subheadingSize}em`,
textAlign: this.headingAlign,
marginTop: `${this.headingSpacing}px`,
}
},
ogAuthorStyle() {
let value = {
background: 'none',
textAlign: this.authorAlign,
color: this.authorTextColor,
marginBottom: `${this.authorMarginBottom}px`,
padding: `${this.authorPadding}px`,
marginLeft: `-${this.contentPadding}px`,
fontSize: `${this.authorSize}em`,
width: `calc(${this.contentPadding * 2}px + 100%)`,
}
if (this.backgroundHover) {
const colors = hexToRgb(this.authorBackgroundColor)
value.background = `rgba(${colors.red}, ${colors.green}, ${colors.blue}, ${this.authorBackgroundOpacity})`
}
if (!this.authorShow) {
value.display = 'none'
}
return value
},
ogAuthorImageStyle(target) {
return {
width: `${this.authorSize}em`,
height: `${this.authorSize}em`,
backgroundImage: `url(${target === 'logo' ? this.logo : this.avatar})`,
}
},
ogAuthorTextStyle() {
let value = {}
if (this.authorAlign === 'left') {
value.marginRight = 'auto !important'
} else if (this.authorAlign === 'center') {
value.marginLeft = 'auto !important'
value.marginRight = 'auto !important'
} else if (this.authorAlign === 'right') {
value.marginLeft = 'auto !important'
}
return value
},
ogImageHoverStyle() {
let background = 'none'
if (this.backgroundHover) {
const colors = hexToRgb(this.backgroundHover)
background = `rgba(${colors.red}, ${colors.green}, ${colors.blue}, ${this.backgroundHoverOpacity})`
}
return {
background,
aspectRatio: `${this.width}/${this.height}`
}
},
ogContentStyle() {
return {
paddingTop: `${this.contentPadding}px`,
paddingRight: `${this.contentPadding}px`,
paddingLeft: `${this.contentPadding}px`,
}
},
setSizes(width, height) {
this.width = width
this.height = height
},
downloadAsPng() {
return this.download(toPng, 'image.png')
},
downloadAsJpeg() {
return this.download(toJpeg, 'image.jpg')
},
download(callback, filename) {
const element = this.$refs.render
callback(element, {
canvasWidth: this.width,
canvasHeight: this.height,
quality: 1,
})
.then((dataUrl) => {
const link = document.createElement('a')
link.setAttribute('href', dataUrl)
link.setAttribute('download', filename)
link.click()
})
.catch((error) => {
element.classList.toggle('og--scale', false)
console.error('oops, something went wrong!', error);
})
},
updateOgMaxWidth() {
this.$refs.og.style.display = 'none'
this.ogMaxWidth = this.$refs.ogwrapper.offsetWidth - 30
this.ogMaxHeight = this.$refs.tools.offsetHeight
this.$refs.og.style.display = 'block'
},
}
}
</script>
<style>
.tool {
max-height: calc(100vh - 200px);
overflow: auto;
}
.tools {
height: calc(100vh - 200px);
width: 443px;
}
.og {
margin: auto;
}
.og-wrapper {
width: calc(100vw - 540px);
}
.og-svg {
overflow: hidden;
box-sizing: border-box;
}
.og-svg > foreignObject {
overflow: hidden;
}
.og-wrapper-content {
background-color: #c1c1c1;
background-image: linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%), linear-gradient(45deg, #646464 25%, transparent 25%, transparent 75%, #646464 75%);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
text-align: center;
}
.og-heading {
font-size: 5em;
}
.og-subheading {
font-size: 3em;
}
.og-image--footer img {
background-size: cover;
background-position: center center;
}
.og-image-hover {
overflow: hidden;
}
</style>