From 5105b3287f88fb64ce32e0ae33a2cbfb35c6aff4 Mon Sep 17 00:00:00 2001 From: Nattsu39 Date: Wed, 11 Sep 2024 19:22:31 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=96=B0=E5=A2=9E=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E8=A7=86=E7=82=B9=E4=BD=8D=E7=BD=AE=E9=80=89?= =?UTF-8?q?=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/commands/export-spine-animation.ts | 19 ++++++++++-- src/handler.ts | 23 ++++++--------- src/renderer.ts | 34 +++++++--------------- src/utils.ts | 22 ++++++++++++-- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/src/cli/commands/export-spine-animation.ts b/src/cli/commands/export-spine-animation.ts index e860187..4404817 100644 --- a/src/cli/commands/export-spine-animation.ts +++ b/src/cli/commands/export-spine-animation.ts @@ -1,5 +1,5 @@ import { CommandModule } from "yargs" -import { extractKeysToArray } from "@/utils.js"; +import { extractKeysToArray, parseVector2 as parseVector2Arg } from "@/utils.js"; import { Exporters } from "@/exporter.js"; import { SpineAnimationExportOptions, exportSpineAnimation } from "@/handler.js"; import { KebabOptions, CommandOptions } from "../type-hind.js"; @@ -38,8 +38,23 @@ export default >{ alias: "c", type: "string", default: null, + coerce: (arg) => { + if (typeof arg !== 'string') return + const vector2 = parseVector2Arg(arg) + return { width: vector2.x, height: vector2.y } + }, desc: "If set, old-style cropping is used, i.e. content that exceeds the canvas size will not be rendered. By default, AABB's min-max vertex positioning rendering range is used.", }, + "view-position": { + type: "string", + default: null, + coerce(arg) { + if (typeof arg !== 'string') return + const vector2 = parseVector2Arg(arg) + return { x: vector2.x, y: vector2.y } + }, + desc: "Set the viewpoint manually. Useful when setting a smaller canvas size." + }, "selected-animation": { alias: "s", array: true, @@ -73,7 +88,7 @@ export default >{ type: "number", default: 2, desc: "Maximum number of concurrencies for export functions" - } + }, }) .example([ ["$0 --export-type gif assets/", "Render assets in ./assets/ and export to GIF."], diff --git a/src/handler.ts b/src/handler.ts index 6a6158f..43398d5 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -2,26 +2,19 @@ import fs from "fs/promises"; import { createCanvas } from "node-canvas-webgl"; import { FfmpegFrameExporter } from "./exporter.js"; import { AssetPath, SpineRenderer, TexturePath, loadTexture } from "./renderer.js"; -import { formatOutputPath, traverseDir, Viewsize } from "./utils.js"; +import { formatOutputPath, traverseDir, ViewPosition, ViewSize } from "./utils.js"; import { TExporterType } from "./exporter.js"; -import { AssetManager, ManagedWebGLRenderingContext } from "@node-spine-runtimes/webgl-3.8.99"; +import { AssetManager, ManagedWebGLRenderingContext, Vector2 } from "@node-spine-runtimes/webgl-3.8.99"; import sharp from "sharp"; import path from "path"; import { formatString } from "./utils.js"; -export function parseCanvasSize(size: string): Viewsize { - const canvasSize = size.split("x"); - if (canvasSize.length !== 2) { - throw new Error("Canvas size format error! \n" + "Correct format: [width]x[height], for example 500x500."); - } - return { width: parseInt(canvasSize[0]), height: parseInt(canvasSize[1]) }; -} - export interface SpineAnimationExportOptions { outputPath?: string; exportType: TExporterType; exporterMaxConcurrent?: number; - canvasSize?: string | null; + canvasSize?: ViewSize; + viewPosition?: ViewPosition, selectedAnimation?: string[]; preMultipliedAlpha: boolean; scale?: number; @@ -35,6 +28,7 @@ export async function exportSpineAnimation(inputDir: string, options: SpineAnima exportType, exporterMaxConcurrent = 2, canvasSize, + viewPosition, selectedAnimation = [], preMultipliedAlpha = false, fps = 30, @@ -64,13 +58,14 @@ export async function exportSpineAnimation(inputDir: string, options: SpineAnima if (selectedAnimation.length && !selectedAnimation.includes(animationName)) { continue; } - const viewsize = typeof canvasSize === "string" ? parseCanvasSize(canvasSize) : undefined; + const viewSize = canvasSize; console.log(`${assetProcess}Start rendering the animation '${animationName}' of asset '${assetName}'...`); const animationFrames = renderer.render({ skeleton, state, animationName, - viewsize, + viewSize, + viewPosition, fps, endPosition, }); @@ -83,7 +78,7 @@ export async function exportSpineAnimation(inputDir: string, options: SpineAnima { outputPath: formatOutputPath(outputPath, formatObject), fps, - autoCrop: viewsize !== undefined, + autoCrop: viewSize !== undefined, } ); } diff --git a/src/renderer.ts b/src/renderer.ts index 810d634..693ebdc 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -23,7 +23,7 @@ import { } from "@node-spine-runtimes/webgl-3.8.99"; import fs from "fs"; import path from "path"; -import { sleep, replacePathSpecific, removePathExtension, Viewsize } from "./utils.js"; +import { sleep, replacePathSpecific, removePathExtension, ViewSize, Viewport, ViewPosition } from "./utils.js"; export interface LoadedResult { skeleton: Skeleton; @@ -33,7 +33,8 @@ export interface LoadedResult { export interface RenderOptions extends LoadedResult { animationName?: string; fps: number; - viewsize?: Viewsize; + viewSize?: ViewSize; + viewPosition?: ViewPosition; endPosition?: number; } @@ -64,13 +65,6 @@ export class TimeKeeper { } } -export interface Viewport { - x: number; - y: number; - width: number; - height: number; -} - function calculateAnimationViewport(animation: Animation, skeleton: Skeleton, fps: number): Viewport { skeleton.setToSetupPose(); @@ -164,34 +158,28 @@ export class SpineRenderer { }; const render = () => { - this.renderer.camera.position.x = viewport.x + viewport.width / 2; - this.renderer.camera.position.y = viewport.y + viewport.height / 2; + this.renderer.camera.position.x = x_position; + this.renderer.camera.position.y = y_position; this.gl.clearColor(0, 0, 0, 0); this.gl.clear(this.gl.COLOR_BUFFER_BIT); this.renderer.begin(); this.renderer.drawSkeleton(skeleton, true); this.renderer.end(); }; - let { skeleton, state, animationName = skeleton.data.animations[0].name, fps, viewsize, endPosition = Infinity } = options; + let { skeleton, state, animationName = skeleton.data.animations[0].name, fps, viewSize: viewsize, viewPosition, endPosition = Infinity } = options; state.setAnimation(0, animationName, false); const viewport = calculateAnimationViewport(skeleton.data.findAnimation(animationName)!, skeleton, fps); - if (viewsize === undefined) { - this.canvas.width = Math.round(viewport.width); - this.canvas.height = Math.round(viewport.height); - } - else { - this.canvas.width = viewsize.width; - this.canvas.height = viewsize.height; - } - + this.canvas.width = viewsize?.width || Math.round(viewport.width); + this.canvas.height = viewsize?.height || Math.round(viewport.width) if (this.canvas.width % 2 !== 0) this.canvas.width += 1; if (this.canvas.height % 2 !== 0) this.canvas.height += 1; - + const x_position = viewPosition?.x || viewport.x + viewport.width / 2 + const y_position = viewPosition?.y || viewport.y + viewport.height / 2 + let isComplete: boolean = false; const renderingEnd = () => { isComplete = true }; - state.addListener({ complete: renderingEnd, }); diff --git a/src/utils.ts b/src/utils.ts index 42c4753..600f655 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,7 @@ import fs from "fs/promises"; import { AssetPathGroup } from "./renderer.js"; import path from "path"; +import { Vector2 } from "@node-spine-runtimes/webgl-3.8.99"; export const sleep = (waitTimeInMs: number) => new Promise((resolve) => setTimeout(resolve, waitTimeInMs)); @@ -90,7 +91,24 @@ export function defer(): Deferred { return { resolve, reject, promise }; } -export interface Viewsize { +export interface ViewSize { width: number; height: number; -} \ No newline at end of file +} + +export interface ViewPosition { + x: number; + y: number; +} + +export type Viewport = ViewSize & ViewPosition + +export function parseVector2(arg: string): Vector2 { + arg = arg.replaceAll(`'`, '') + arg = arg.replaceAll(`"`, '') + const twoArgs = arg.split("x"); + if (twoArgs.length !== 2) { + throw new Error("Arg format error! \n" + "Correct format: [arg1]x[arg2]"); + } + return new Vector2(parseInt(twoArgs[0]), parseInt(twoArgs[1])) +}