-
-
Save Dozorengel/79f64389480051ada931c26974e3efe0 to your computer and use it in GitHub Desktop.
Open Graph image generator (React, HTML5 Canvas)
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
import React, { FunctionComponent, useEffect, useRef } from 'react' | |
interface Props extends React.InputHTMLAttributes<HTMLCanvasElement> { | |
draw: (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => Promise<void> | |
width: number | |
height: number | |
} | |
const Canvas: FunctionComponent<Props> = ({ draw, width, height, ...props }) => { | |
const canvasRef = useRef<HTMLCanvasElement>(null) | |
useEffect(() => { | |
const canvas = canvasRef.current | |
const context = canvas.getContext('2d') | |
canvas.width = width | |
canvas.height = height | |
const drawFunc = async () => { | |
await draw(context, canvas) | |
} | |
drawFunc() | |
}, [draw]) | |
return <canvas ref={canvasRef} {...props} /> | |
} | |
export default Canvas |
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
import React, { Dispatch, FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' | |
import { Checkbox, Label, Textarea, RadioGroup } from '../form' | |
import Canvas from './canvas' | |
import reminderLogo from '../../images/logo.png' | |
import defaultBackground from '../../images/og-background.png' | |
import c from './og-widget.sass' | |
export interface ogImagePost { | |
background_type: 'bg' | 'image' | |
has_logo: boolean | |
text: string | |
image_id?: number | |
} | |
interface Props { | |
name: string | |
data: ogImagePost | |
title: string | |
cover: string | |
disabled?: boolean | |
onChange: (e: { name: string; value: any }) => void | |
emitOgImage: Dispatch<any> | |
setTouched: Dispatch<boolean> | |
} | |
const CANVAS_WIDTH = 1280 | |
const CANVAS_HEIGHT = 670 | |
export const OgWidget: FunctionComponent<Props> = ({ | |
name, | |
data, | |
title, | |
cover, | |
disabled = false, | |
onChange, | |
emitOgImage, | |
setTouched, | |
}) => { | |
const { background_type, has_logo, text: og_text } = data | |
const bgTypes = { | |
bg: 'bg', | |
image: 'image', | |
} | |
const bgItems = [ | |
{ value: bgTypes.bg, label: 'Фон' }, | |
{ value: bgTypes.image, label: 'Картинка' }, | |
] | |
const [background, setBackground] = useState(background_type || bgTypes.bg) | |
const [logo, setLogo] = useState(has_logo ?? true) | |
const [text, setText] = useState(og_text || title || '') | |
const isFirstRun = useRef(true) | |
const linkTitle = useRef(!Object.keys(data).length || title === text) | |
useEffect(() => { | |
if (linkTitle.current) { | |
setText(title) | |
} | |
}, [title]) | |
useEffect(() => { | |
if (cover) { | |
setBackground(bgTypes.image) | |
setLogo(false) | |
// force re-render | |
text ? setText('') : setText(' ') | |
} | |
}, [cover]) | |
const drawBackground = (ctx: CanvasRenderingContext2D) => { | |
return new Promise<string>((resolve) => { | |
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) | |
const img = new Image() | |
img.onload = () => { | |
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height) | |
resolve('bg loaded') | |
} | |
img.crossOrigin = 'anonymous' | |
img.src = background !== bgTypes.bg && cover ? cover : defaultBackground | |
}) | |
} | |
const drawLogo = (ctx: CanvasRenderingContext2D) => { | |
return new Promise<string>((resolve) => { | |
const logo = new Image() | |
logo.onload = () => { | |
ctx.drawImage(logo, 60, 60) | |
resolve('logo loaded') | |
} | |
logo.crossOrigin = 'anonymous' | |
logo.src = reminderLogo | |
}) | |
} | |
const drawText = (ctx: CanvasRenderingContext2D) => { | |
return new Promise<string>((resolve) => { | |
if (background === bgTypes.image && text.trim()) { | |
// Mask | |
let grd = ctx.createLinearGradient(0, 300, 0, ctx.canvas.height) | |
grd.addColorStop(0, 'transparent') | |
grd.addColorStop(0.3, 'rgba(0, 0, 0, 0.6)') | |
ctx.fillStyle = grd | |
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height) | |
} | |
// Text | |
ctx.fillStyle = 'white' | |
ctx.font = 'bold 64px cofo' | |
wrapText(ctx, text, 50, 420, 1100, 70) | |
resolve('text drawn') | |
}) | |
} | |
const wrapText = (ctx: CanvasRenderingContext2D, text, x, y, maxWidth, lineHeight) => { | |
const words = text.split(' ') | |
let line = '' | |
for (let n = 0; n < words.length; n++) { | |
const testLine = line + words[n] + ' ' | |
const metrics = ctx.measureText(testLine) | |
const testWidth = metrics.width | |
if (testWidth > maxWidth && n > 0) { | |
ctx.fillText(line, x, y) | |
line = words[n] + ' ' | |
y += lineHeight | |
} else { | |
line = testLine | |
} | |
} | |
ctx.fillText(line, x, y) | |
} | |
const draw = useCallback( | |
async (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement) => { | |
await drawBackground(ctx) | |
await Promise.all([logo && drawLogo(ctx), drawText(ctx)]) | |
emitOgImage(await new Promise((r) => canvas.toBlob(r))) | |
if (isFirstRun.current && Object.keys(data).length) { | |
isFirstRun.current = false | |
} else { | |
onChange({ | |
name, | |
value: { | |
background_type: background, | |
has_logo: logo, | |
text, | |
}, | |
}) | |
setTouched(true) | |
} | |
}, | |
[background, logo, text], | |
) | |
return ( | |
<div className={c.container}> | |
<Canvas draw={draw} width={CANVAS_WIDTH} height={CANVAS_HEIGHT} /> | |
<div className={c.controlBox}> | |
{cover && ( | |
<RadioGroup | |
items={bgItems} | |
value={background} | |
name='bg_types' | |
onChange={(e) => setBackground(e.target.value)} | |
disabled={disabled} | |
/> | |
)} | |
<Label title='Логотип'> | |
<Checkbox | |
checked={logo} | |
onChange={(e) => setLogo(e.target.checked)} | |
disabled={disabled} | |
/> | |
</Label> | |
<Label title='Текст'> | |
<Textarea | |
value={text} | |
onChange={(e) => { | |
linkTitle.current = false | |
setText(e.target.value) | |
}} | |
placeholder='Заголовок' | |
disabled={disabled} | |
/> | |
</Label> | |
</div> | |
</div> | |
) | |
} |
Author
Dozorengel
commented
Mar 29, 2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment