Slefhostet und postgres
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,286 +1,286 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GreenLens Social Video Renderer</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #0b1711;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 360px;
|
||||
height: 640px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
image-rendering: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="720" height="1280"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const WIDTH = canvas.width;
|
||||
const HEIGHT = canvas.height;
|
||||
|
||||
function roundedRectPath(x, y, w, h, r) {
|
||||
const radius = Math.min(r, w / 2, h / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
||||
ctx.arcTo(x, y + h, x, y, radius);
|
||||
ctx.arcTo(x, y, x + w, y, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function drawBackground(accentA, accentB) {
|
||||
const bg = ctx.createLinearGradient(0, 0, WIDTH, HEIGHT);
|
||||
bg.addColorStop(0, '#07110c');
|
||||
bg.addColorStop(0.55, accentA || '#10251a');
|
||||
bg.addColorStop(1, accentB || '#29553f');
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(80, 120, 180, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalAlpha = 0.12;
|
||||
ctx.beginPath();
|
||||
ctx.arc(WIDTH - 40, HEIGHT - 120, 260, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function drawImageCover(img, x, y, w, h, scale = 1, offsetX = 0, offsetY = 0) {
|
||||
const imgRatio = img.width / img.height;
|
||||
const boxRatio = w / h;
|
||||
let drawW;
|
||||
let drawH;
|
||||
|
||||
if (imgRatio > boxRatio) {
|
||||
drawH = h * scale;
|
||||
drawW = drawH * imgRatio;
|
||||
} else {
|
||||
drawW = w * scale;
|
||||
drawH = drawW / imgRatio;
|
||||
}
|
||||
|
||||
const dx = x + (w - drawW) / 2 + offsetX;
|
||||
const dy = y + (h - drawH) / 2 + offsetY;
|
||||
ctx.drawImage(img, dx, dy, drawW, drawH);
|
||||
}
|
||||
|
||||
function drawCard(scene, progress) {
|
||||
const imageX = 52;
|
||||
const imageY = 160;
|
||||
const imageW = WIDTH - 104;
|
||||
const imageH = 700;
|
||||
const scale = 1.02 + progress * 0.07;
|
||||
const slide = (1 - progress) * 22;
|
||||
|
||||
ctx.save();
|
||||
roundedRectPath(imageX, imageY, imageW, imageH, 42);
|
||||
ctx.clip();
|
||||
drawImageCover(scene.image, imageX, imageY, imageW, imageH, scale, 0, slide);
|
||||
|
||||
const overlay = ctx.createLinearGradient(0, imageY, 0, imageY + imageH);
|
||||
overlay.addColorStop(0, 'rgba(4, 10, 7, 0.08)');
|
||||
overlay.addColorStop(0.55, 'rgba(4, 10, 7, 0.18)');
|
||||
overlay.addColorStop(1, 'rgba(4, 10, 7, 0.7)');
|
||||
ctx.fillStyle = overlay;
|
||||
ctx.fillRect(imageX, imageY, imageW, imageH);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||||
ctx.lineWidth = 2;
|
||||
roundedRectPath(imageX, imageY, imageW, imageH, 42);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawLogo(logo, progress) {
|
||||
const size = 84;
|
||||
const x = 56;
|
||||
const y = 56 - (1 - progress) * 10;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = progress;
|
||||
roundedRectPath(x, y, size, size, 24);
|
||||
ctx.clip();
|
||||
ctx.drawImage(logo, x, y, size, size);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawText(scene, progress) {
|
||||
const titleY = 942 - (1 - progress) * 18;
|
||||
const subtitleY = 1128 - (1 - progress) * 14;
|
||||
const alpha = clamp(progress * 1.15, 0, 1);
|
||||
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
if (scene.badge) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(217,245,208,0.13)';
|
||||
roundedRectPath(56, 922 - 76, 220, 48, 22);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.font = '700 24px Arial';
|
||||
ctx.fillText(scene.badge.toUpperCase(), 76, 878);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '700 64px Arial';
|
||||
wrapText(scene.title, 56, titleY, WIDTH - 112, 74);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.84)';
|
||||
ctx.font = '400 30px Arial';
|
||||
wrapText(scene.subtitle, 56, subtitleY, WIDTH - 112, 42);
|
||||
|
||||
if (scene.cta) {
|
||||
ctx.save();
|
||||
const ctaY = HEIGHT - 98;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.10)';
|
||||
roundedRectPath(56, ctaY - 38, WIDTH - 112, 58, 20);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.font = '700 24px Arial';
|
||||
ctx.fillText(scene.cta, 82, ctaY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function wrapText(text, x, y, maxWidth, lineHeight) {
|
||||
const words = text.split(/\s+/);
|
||||
let line = '';
|
||||
let lineY = y;
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = line ? `${line} ${word}` : word;
|
||||
const width = ctx.measureText(testLine).width;
|
||||
if (width > maxWidth && line) {
|
||||
ctx.fillText(line, x, lineY);
|
||||
line = word;
|
||||
lineY += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
ctx.fillText(line, x, lineY);
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function renderVideo(payload) {
|
||||
const logo = await loadImage(payload.logo);
|
||||
const scenes = await Promise.all(
|
||||
payload.scenes.map(async (scene) => ({
|
||||
...scene,
|
||||
image: await loadImage(scene.image),
|
||||
}))
|
||||
);
|
||||
|
||||
const stream = canvas.captureStream(payload.fps || 30);
|
||||
const mimeType = 'video/webm;codecs=vp9';
|
||||
const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 5_000_000 });
|
||||
const chunks = [];
|
||||
|
||||
recorder.ondataavailable = (event) => {
|
||||
if (event.data && event.data.size > 0) {
|
||||
chunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
const stopped = new Promise((resolve) => {
|
||||
recorder.onstop = resolve;
|
||||
});
|
||||
|
||||
const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
|
||||
const startedAt = performance.now();
|
||||
recorder.start(250);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
function frame(now) {
|
||||
const elapsed = now - startedAt;
|
||||
const cappedElapsed = Math.min(elapsed, totalDuration);
|
||||
|
||||
let offset = 0;
|
||||
let activeScene = scenes[scenes.length - 1];
|
||||
let sceneElapsed = activeScene.durationMs;
|
||||
|
||||
for (const scene of scenes) {
|
||||
if (cappedElapsed <= offset + scene.durationMs) {
|
||||
activeScene = scene;
|
||||
sceneElapsed = cappedElapsed - offset;
|
||||
break;
|
||||
}
|
||||
offset += scene.durationMs;
|
||||
}
|
||||
|
||||
const normalized = clamp(sceneElapsed / activeScene.durationMs, 0, 1);
|
||||
const intro = easeOutCubic(clamp(normalized / 0.24, 0, 1));
|
||||
|
||||
drawBackground(payload.accentA, payload.accentB);
|
||||
drawCard(activeScene, normalized);
|
||||
drawLogo(logo, intro);
|
||||
drawText(activeScene, intro);
|
||||
|
||||
if (elapsed < totalDuration) {
|
||||
requestAnimationFrame(frame);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
recorder.stop();
|
||||
await stopped;
|
||||
|
||||
const blob = new Blob(chunks, { type: mimeType });
|
||||
const uploadUrl = `/upload?name=${encodeURIComponent(payload.outputName)}`;
|
||||
const response = await fetch(uploadUrl, { method: 'POST', body: blob });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
window.renderVideo = renderVideo;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GreenLens Social Video Renderer</title>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #0b1711;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 360px;
|
||||
height: 640px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
image-rendering: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas" width="720" height="1280"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const WIDTH = canvas.width;
|
||||
const HEIGHT = canvas.height;
|
||||
|
||||
function roundedRectPath(x, y, w, h, r) {
|
||||
const radius = Math.min(r, w / 2, h / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
||||
ctx.arcTo(x, y + h, x, y, radius);
|
||||
ctx.arcTo(x, y, x + w, y, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function drawBackground(accentA, accentB) {
|
||||
const bg = ctx.createLinearGradient(0, 0, WIDTH, HEIGHT);
|
||||
bg.addColorStop(0, '#07110c');
|
||||
bg.addColorStop(0.55, accentA || '#10251a');
|
||||
bg.addColorStop(1, accentB || '#29553f');
|
||||
ctx.fillStyle = bg;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(80, 120, 180, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalAlpha = 0.12;
|
||||
ctx.beginPath();
|
||||
ctx.arc(WIDTH - 40, HEIGHT - 120, 260, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function drawImageCover(img, x, y, w, h, scale = 1, offsetX = 0, offsetY = 0) {
|
||||
const imgRatio = img.width / img.height;
|
||||
const boxRatio = w / h;
|
||||
let drawW;
|
||||
let drawH;
|
||||
|
||||
if (imgRatio > boxRatio) {
|
||||
drawH = h * scale;
|
||||
drawW = drawH * imgRatio;
|
||||
} else {
|
||||
drawW = w * scale;
|
||||
drawH = drawW / imgRatio;
|
||||
}
|
||||
|
||||
const dx = x + (w - drawW) / 2 + offsetX;
|
||||
const dy = y + (h - drawH) / 2 + offsetY;
|
||||
ctx.drawImage(img, dx, dy, drawW, drawH);
|
||||
}
|
||||
|
||||
function drawCard(scene, progress) {
|
||||
const imageX = 52;
|
||||
const imageY = 160;
|
||||
const imageW = WIDTH - 104;
|
||||
const imageH = 700;
|
||||
const scale = 1.02 + progress * 0.07;
|
||||
const slide = (1 - progress) * 22;
|
||||
|
||||
ctx.save();
|
||||
roundedRectPath(imageX, imageY, imageW, imageH, 42);
|
||||
ctx.clip();
|
||||
drawImageCover(scene.image, imageX, imageY, imageW, imageH, scale, 0, slide);
|
||||
|
||||
const overlay = ctx.createLinearGradient(0, imageY, 0, imageY + imageH);
|
||||
overlay.addColorStop(0, 'rgba(4, 10, 7, 0.08)');
|
||||
overlay.addColorStop(0.55, 'rgba(4, 10, 7, 0.18)');
|
||||
overlay.addColorStop(1, 'rgba(4, 10, 7, 0.7)');
|
||||
ctx.fillStyle = overlay;
|
||||
ctx.fillRect(imageX, imageY, imageW, imageH);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||||
ctx.lineWidth = 2;
|
||||
roundedRectPath(imageX, imageY, imageW, imageH, 42);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawLogo(logo, progress) {
|
||||
const size = 84;
|
||||
const x = 56;
|
||||
const y = 56 - (1 - progress) * 10;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = progress;
|
||||
roundedRectPath(x, y, size, size, 24);
|
||||
ctx.clip();
|
||||
ctx.drawImage(logo, x, y, size, size);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawText(scene, progress) {
|
||||
const titleY = 942 - (1 - progress) * 18;
|
||||
const subtitleY = 1128 - (1 - progress) * 14;
|
||||
const alpha = clamp(progress * 1.15, 0, 1);
|
||||
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
if (scene.badge) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = 'rgba(217,245,208,0.13)';
|
||||
roundedRectPath(56, 922 - 76, 220, 48, 22);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.font = '700 24px Arial';
|
||||
ctx.fillText(scene.badge.toUpperCase(), 76, 878);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = '700 64px Arial';
|
||||
wrapText(scene.title, 56, titleY, WIDTH - 112, 74);
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.84)';
|
||||
ctx.font = '400 30px Arial';
|
||||
wrapText(scene.subtitle, 56, subtitleY, WIDTH - 112, 42);
|
||||
|
||||
if (scene.cta) {
|
||||
ctx.save();
|
||||
const ctaY = HEIGHT - 98;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.10)';
|
||||
roundedRectPath(56, ctaY - 38, WIDTH - 112, 58, 20);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#d9f5d0';
|
||||
ctx.font = '700 24px Arial';
|
||||
ctx.fillText(scene.cta, 82, ctaY);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function wrapText(text, x, y, maxWidth, lineHeight) {
|
||||
const words = text.split(/\s+/);
|
||||
let line = '';
|
||||
let lineY = y;
|
||||
|
||||
for (const word of words) {
|
||||
const testLine = line ? `${line} ${word}` : word;
|
||||
const width = ctx.measureText(testLine).width;
|
||||
if (width > maxWidth && line) {
|
||||
ctx.fillText(line, x, lineY);
|
||||
line = word;
|
||||
lineY += lineHeight;
|
||||
} else {
|
||||
line = testLine;
|
||||
}
|
||||
}
|
||||
|
||||
if (line) {
|
||||
ctx.fillText(line, x, lineY);
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function renderVideo(payload) {
|
||||
const logo = await loadImage(payload.logo);
|
||||
const scenes = await Promise.all(
|
||||
payload.scenes.map(async (scene) => ({
|
||||
...scene,
|
||||
image: await loadImage(scene.image),
|
||||
}))
|
||||
);
|
||||
|
||||
const stream = canvas.captureStream(payload.fps || 30);
|
||||
const mimeType = 'video/webm;codecs=vp9';
|
||||
const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 5_000_000 });
|
||||
const chunks = [];
|
||||
|
||||
recorder.ondataavailable = (event) => {
|
||||
if (event.data && event.data.size > 0) {
|
||||
chunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
const stopped = new Promise((resolve) => {
|
||||
recorder.onstop = resolve;
|
||||
});
|
||||
|
||||
const totalDuration = scenes.reduce((sum, scene) => sum + scene.durationMs, 0);
|
||||
const startedAt = performance.now();
|
||||
recorder.start(250);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
function frame(now) {
|
||||
const elapsed = now - startedAt;
|
||||
const cappedElapsed = Math.min(elapsed, totalDuration);
|
||||
|
||||
let offset = 0;
|
||||
let activeScene = scenes[scenes.length - 1];
|
||||
let sceneElapsed = activeScene.durationMs;
|
||||
|
||||
for (const scene of scenes) {
|
||||
if (cappedElapsed <= offset + scene.durationMs) {
|
||||
activeScene = scene;
|
||||
sceneElapsed = cappedElapsed - offset;
|
||||
break;
|
||||
}
|
||||
offset += scene.durationMs;
|
||||
}
|
||||
|
||||
const normalized = clamp(sceneElapsed / activeScene.durationMs, 0, 1);
|
||||
const intro = easeOutCubic(clamp(normalized / 0.24, 0, 1));
|
||||
|
||||
drawBackground(payload.accentA, payload.accentB);
|
||||
drawCard(activeScene, normalized);
|
||||
drawLogo(logo, intro);
|
||||
drawText(activeScene, intro);
|
||||
|
||||
if (elapsed < totalDuration) {
|
||||
requestAnimationFrame(frame);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
recorder.stop();
|
||||
await stopped;
|
||||
|
||||
const blob = new Blob(chunks, { type: mimeType });
|
||||
const uploadUrl = `/upload?name=${encodeURIComponent(payload.outputName)}`;
|
||||
const response = await fetch(uploadUrl, { method: 'POST', body: blob });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Upload failed with status ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
window.renderVideo = renderVideo;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user