Skip to content

Instantly share code, notes, and snippets.

@Dozorengel
Created March 13, 2022 15:30
Show Gist options
  • Save Dozorengel/79f64389480051ada931c26974e3efe0 to your computer and use it in GitHub Desktop.
Save Dozorengel/79f64389480051ada931c26974e3efe0 to your computer and use it in GitHub Desktop.
Open Graph image generator (React, HTML5 Canvas)
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
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>
)
}
@Dozorengel
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment