Improve MineGeo UI and application flows
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 2m37s
This commit is contained in:
274
cinematics/composition.mjs
Normal file
274
cinematics/composition.mjs
Normal file
@@ -0,0 +1,274 @@
|
||||
import { enter, shots } from "./motion.mjs";
|
||||
|
||||
const cream = "#f0efda",
|
||||
green = "#b7df8a",
|
||||
gold = "#dcc28a";
|
||||
function text(ctx, value, x, y, size, color = cream) {
|
||||
ctx.font = `${size}px Pixel`;
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillText(value, x, y);
|
||||
}
|
||||
function label(ctx, value, x, y, size = 11, color = "#a8bda3") {
|
||||
ctx.font = `${size}px Arial`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = "left";
|
||||
ctx.letterSpacing = "2px";
|
||||
ctx.fillText(value, x, y);
|
||||
ctx.letterSpacing = "0px";
|
||||
}
|
||||
function cross(ctx, x, y) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 5, y);
|
||||
ctx.lineTo(x + 5, y);
|
||||
ctx.moveTo(x, y - 5);
|
||||
ctx.lineTo(x, y + 5);
|
||||
ctx.stroke();
|
||||
}
|
||||
function word(ctx, value, x, y, size, time, start, end, color = cream) {
|
||||
// 50ms stagger: the word reads as one gesture, followed by a long quiet hold.
|
||||
let offset = 0;
|
||||
ctx.font = `${size}px Pixel`;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const a = enter(time, start + i * 0.05);
|
||||
const out = enter(time, end - 0.25);
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= a * (1 - out);
|
||||
text(ctx, value[i], x + offset, y + (1 - a) * 12 + out * 12, size, color);
|
||||
ctx.restore();
|
||||
offset += ctx.measureText(value[i]).width;
|
||||
}
|
||||
}
|
||||
function mark(ctx, x, y, size, time) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.strokeStyle = "rgba(209,217,174,.5)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, size, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const a = (i / 32) * Math.PI * 2,
|
||||
r = size - (i % 8 === 0 ? 9 : 3);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.sin(a) * r, Math.cos(a) * r);
|
||||
ctx.lineTo(Math.sin(a) * size, Math.cos(a) * size);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.rotate(Math.sin(time) * 0.09 - 0.35);
|
||||
ctx.fillStyle = gold;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -size * 0.6);
|
||||
ctx.lineTo(size * 0.13, 0);
|
||||
ctx.lineTo(0, size * 0.1);
|
||||
ctx.lineTo(-size * 0.13, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fillStyle = "#57725b";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, size * 0.6);
|
||||
ctx.lineTo(size * 0.13, 0);
|
||||
ctx.lineTo(0, -size * 0.1);
|
||||
ctx.lineTo(-size * 0.13, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
function screenshot(ctx, image, x, y, w, h) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "#07130e";
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x - 1, y - 1, w + 2, h + 2, 8);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "rgba(181,205,154,.5)";
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, w, h, 7);
|
||||
ctx.clip();
|
||||
ctx.drawImage(
|
||||
image,
|
||||
0,
|
||||
0,
|
||||
image.naturalWidth,
|
||||
image.naturalHeight,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
);
|
||||
ctx.restore();
|
||||
}
|
||||
function screenshotShot(ctx, shot, image, t) {
|
||||
const a = enter(t, shot.start - 0.125),
|
||||
exit = enter(t, shot.end - 0.125);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = a * (1 - exit);
|
||||
label(ctx, shot.chapter, 64, 30, 10, gold);
|
||||
text(ctx, shot.title, 62, 76, 34, cream);
|
||||
ctx.font = "15px Arial";
|
||||
ctx.fillStyle = "#aebfa6";
|
||||
ctx.fillText(shot.subtitle, 65, 100);
|
||||
// Readable data stays still after a 250ms settle; no continuous pan or zoom.
|
||||
const settle = enter(t, shot.start + 0.05),
|
||||
leave = enter(t, shot.end - 0.125);
|
||||
const w = 1040,
|
||||
h = 585,
|
||||
x = 120,
|
||||
y = 121;
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= settle;
|
||||
ctx.translate(640, y + h / 2 + 8 * (1 - settle) + 8 * leave);
|
||||
const scale = 0.96 + 0.04 * settle;
|
||||
ctx.scale(scale, scale);
|
||||
ctx.translate(-640, -y - h / 2);
|
||||
screenshot(ctx, image, x, y, w, h);
|
||||
if (shot.image === "game") {
|
||||
const focus =
|
||||
enter(t, shot.start + 2, 0.25) * (1 - enter(t, shot.end - 0.4, 0.25));
|
||||
ctx.globalAlpha *= focus;
|
||||
ctx.strokeStyle = gold;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x + w * 0.771, y + h * 0.496, w * 0.222, h * 0.482);
|
||||
ctx.fillStyle = "#0b1b13";
|
||||
ctx.fillRect(x + 24, y + h - 66, 310, 38);
|
||||
label(ctx, "LOOK AROUND. PLACE YOUR PIN.", x + 37, y + h - 42, 10, gold);
|
||||
}
|
||||
ctx.restore();
|
||||
ctx.restore();
|
||||
}
|
||||
function brandShot(ctx, shot, t, closing) {
|
||||
const a = enter(t, shot.start),
|
||||
exit = enter(t, shot.end - 0.25);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = a * (1 - exit);
|
||||
label(ctx, shot.chapter, 66, 215, 11, gold);
|
||||
if (closing) {
|
||||
word(ctx, "MINEGEO", 60, 322, 80, t, shot.start + 0.05, shot.end, green);
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= enter(t, shot.start + 0.45);
|
||||
ctx.font = "20px Arial";
|
||||
ctx.fillStyle = "#becdb3";
|
||||
ctx.fillText(shot.subtitle, 66, 369);
|
||||
ctx.restore();
|
||||
const cta = enter(t, shot.start + 0.7);
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= cta;
|
||||
ctx.translate(0, 8 * (1 - cta));
|
||||
ctx.fillStyle = green;
|
||||
ctx.fillRect(65, 409, 330, 57);
|
||||
text(ctx, "MINEGEO.SITE", 88, 448, 25, "#112215");
|
||||
label(ctx, "FREE TO EXPLORE / FIVE ROUNDS", 66, 500, 10);
|
||||
ctx.restore();
|
||||
} else {
|
||||
word(ctx, "GET LOST.", 60, 313, 68, t, shot.start + 0.05, shot.end);
|
||||
word(
|
||||
ctx,
|
||||
"FIND YOUR WORLD.",
|
||||
60,
|
||||
392,
|
||||
48,
|
||||
t,
|
||||
shot.start + 0.16,
|
||||
shot.end,
|
||||
green,
|
||||
);
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= enter(t, shot.start + 0.7);
|
||||
ctx.font = "17px Arial";
|
||||
ctx.fillStyle = "#becdb3";
|
||||
ctx.fillText(shot.subtitle, 66, 445);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawBanner(ctx, t, pin) {
|
||||
label(ctx, "YOUR NEXT EXPEDITION", 66, 108, 12, gold);
|
||||
// Always legible in Discord: only the scene and targeting sweep loop.
|
||||
text(ctx, "MINEGEO", 60, 233, 82, green);
|
||||
ctx.font = "18px Arial";
|
||||
ctx.fillStyle = "#ccd7bf";
|
||||
ctx.fillText("Get lost. Find your world.", 66, 280);
|
||||
ctx.strokeStyle = "rgba(184,207,160,.35)";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(66, 320);
|
||||
ctx.lineTo(430, 320);
|
||||
ctx.stroke();
|
||||
label(ctx, "MINEGEO.SITE", 66, 350, 13, cream);
|
||||
label(ctx, "EXPLORE / GUESS / DISCOVER", 66, 388, 10);
|
||||
mark(ctx, 1126, 68, 25, (t / 3) * Math.PI * 2);
|
||||
if (pin) {
|
||||
ctx.save();
|
||||
ctx.translate(pin.x, pin.y);
|
||||
const phase = (t / 3) * Math.PI * 2,
|
||||
pulse = 0.5 - 0.5 * Math.cos(phase);
|
||||
ctx.strokeStyle = `rgba(234,210,144,${0.25 + 0.25 * (1 - pulse)})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 20 + pulse * 13, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.rotate(phase);
|
||||
ctx.strokeStyle = "rgba(234,210,144,.65)";
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ctx.rotate(Math.PI / 2);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(28, -7);
|
||||
ctx.lineTo(28, 7);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
export function composeFrame(
|
||||
ctx,
|
||||
source,
|
||||
{ width, height, banner, time: t, screenshots, pin },
|
||||
) {
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.drawImage(source, 0, 0);
|
||||
const scale = width / (banner ? 1200 : 1280),
|
||||
w = width / scale,
|
||||
h = height / scale;
|
||||
ctx.save();
|
||||
ctx.scale(scale, scale);
|
||||
const gradient = ctx.createLinearGradient(0, 0, w, 0);
|
||||
gradient.addColorStop(0, "rgba(5,15,10,.97)");
|
||||
gradient.addColorStop(0.38, "rgba(5,15,10,.86)");
|
||||
gradient.addColorStop(0.8, "rgba(5,15,10,0)");
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
const wash = ctx.createLinearGradient(0, 0, 0, h);
|
||||
wash.addColorStop(0, "rgba(5,15,10,.2)");
|
||||
wash.addColorStop(0.65, "rgba(5,15,10,0)");
|
||||
wash.addColorStop(1, "rgba(5,15,10,.55)");
|
||||
ctx.fillStyle = wash;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.strokeStyle = "rgba(171,196,144,.25)";
|
||||
ctx.lineWidth = 1;
|
||||
cross(ctx, 28, 28);
|
||||
cross(ctx, w - 28, 28);
|
||||
cross(ctx, 28, h - 28);
|
||||
cross(ctx, w - 28, h - 28);
|
||||
if (banner)
|
||||
drawBanner(ctx, t, pin ? { x: pin.x / scale, y: pin.y / scale } : null);
|
||||
else {
|
||||
for (const shot of shots) {
|
||||
if (t < shot.start - 0.125 || t > shot.end + 0.125) continue;
|
||||
if (shot.image) screenshotShot(ctx, shot, screenshots[shot.image], t);
|
||||
else brandShot(ctx, shot, t, shot.start === 23);
|
||||
}
|
||||
if (t < 3 || t >= 23) {
|
||||
mark(ctx, 1188, 81, 29, t * 0.12);
|
||||
label(ctx, "A MINECRAFT GUESSING GAME", 66, 65, 10);
|
||||
}
|
||||
const current = shots.findIndex((s) => t >= s.start && t < s.end);
|
||||
for (let i = 0; i < shots.length; i++) {
|
||||
ctx.fillStyle = i <= current ? "#bfd49b" : "rgba(165,186,143,.3)";
|
||||
ctx.fillRect(28 + i * 16, h - 20, 10, 2);
|
||||
}
|
||||
ctx.fillStyle = `rgba(5,15,10,${1 - enter(t, 0)})`;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.fillStyle = `rgba(5,15,10,${enter(t, 29.75)})`;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
Reference in New Issue
Block a user