shadow-over-bishkek001/cutsceneEditor/src/utils/rendering.ts
2026-06-07 14:40:15 +03:00

80 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ImageSegment, ImagePose } from '../types/cutscene';
import { applyEasing } from '../constants/easings';
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v));
}
function lerp(a: number, b: number, t: number) {
return a + (b - a) * t;
}
export interface SegmentRenderState {
alpha: number;
pose: ImagePose;
}
export function computeSegmentState(seg: ImageSegment, currentMs: number): SegmentRenderState | null {
if (currentMs < seg.startMs || currentMs > seg.endMs) return null;
const duration = seg.endMs - seg.startMs;
const localMs = currentMs - seg.startMs;
const t = duration > 0 ? clamp(localMs / duration, 0, 1) : 1;
const tEased = applyEasing(t, seg.easing);
const pose: ImagePose = {
centerX: lerp(seg.from.centerX, seg.to.centerX, tEased),
centerY: lerp(seg.from.centerY, seg.to.centerY, tEased),
scale: lerp(seg.from.scale, seg.to.scale, tEased),
};
let alpha = 1;
if (seg.fadeInMs > 0 && localMs < seg.fadeInMs) {
alpha = localMs / seg.fadeInMs;
} else if (seg.fadeOutMs > 0 && localMs > duration - seg.fadeOutMs) {
alpha = (duration - localMs) / seg.fadeOutMs;
}
return { alpha: clamp(alpha, 0, 1), pose };
}
/**
* Returns CSS for an absolutely-positioned <img> inside the viewport div.
*
* The image is stretched (object-fit: fill) to exactly logicalW × logicalH
* logical pixels on screen — matching how the game engine maps the full
* texture quad to those dimensions, regardless of the file's natural size.
*
* At scale=1 the logical area fills the viewport (aspect-ratio corrected).
* The viewport's own overflow:hidden clips anything that extends beyond.
*/
export function poseToStyle(
pose: ImagePose,
logicalW: number, // segment.width (e.g. 1280)
logicalH: number, // segment.height (e.g. 720)
containerW: number,
containerH: number,
): React.CSSProperties {
const baseScale = Math.max(containerW / logicalW, containerH / logicalH);
const renderW = logicalW * baseScale * pose.scale;
const renderH = logicalH * baseScale * pose.scale;
const maxOffsetX = Math.max(0, (renderW - containerW) / 2);
const maxOffsetY = Math.max(0, (renderH - containerH) / 2);
const offsetX = clamp((0.5 - pose.centerX) * renderW, -maxOffsetX, maxOffsetX);
// Y axis is inverted: centerY=0 → bottom of image, centerY=1 → top (Y-up convention)
const offsetY = clamp((pose.centerY - 0.5) * renderH, -maxOffsetY, maxOffsetY);
return {
position: 'absolute',
top: '50%',
left: '50%',
width: renderW,
height: renderH,
// Default object-fit (fill) stretches the full texture to these logical
// dimensions, exactly as the game engine renders the quad.
objectFit: 'fill',
transform: `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`,
};
}