mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 15:28:03 -05:00
feat: custom loaders
This commit is contained in:
@@ -8,9 +8,7 @@
|
||||
"scripts": {
|
||||
"prepare": "npm run build",
|
||||
"build": "tsc",
|
||||
"test:serve": "node ./tools/serve.mjs ./test/player.ts",
|
||||
"test:render": "node ./tools/serve.mjs ./test/render.ts",
|
||||
"test:image": "node ./tools/image.mjs ./test/img ./test/animations.json"
|
||||
"test:serve": "node ./tools/serve.mjs ./test/player.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
|
||||
@@ -7,7 +7,7 @@ import {AnimatedGetSet, getset, KonvaNode, threadable} from '../decorators';
|
||||
import {GeneratorHelper} from '../helpers';
|
||||
import {ImageData} from 'canvas';
|
||||
|
||||
interface FrameData {
|
||||
export interface SpriteData {
|
||||
fileName: string;
|
||||
url: string;
|
||||
data: number[];
|
||||
@@ -15,18 +15,12 @@ interface FrameData {
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface SpriteData {
|
||||
animations: Record<string, {frames: FrameData[]}>;
|
||||
skins: Record<string, FrameData>;
|
||||
}
|
||||
|
||||
export interface SpriteConfig extends LayoutShapeConfig {
|
||||
animationData: SpriteData;
|
||||
animation: string;
|
||||
skin?: string;
|
||||
animation: SpriteData[];
|
||||
skin?: SpriteData;
|
||||
mask?: SpriteData;
|
||||
playing?: boolean;
|
||||
fps?: number;
|
||||
mask?: string;
|
||||
maskBlend?: number;
|
||||
}
|
||||
|
||||
@@ -36,21 +30,20 @@ const COMPUTE_CANVAS_SIZE = 1024;
|
||||
|
||||
@KonvaNode()
|
||||
export class Sprite extends LayoutShape {
|
||||
@getset('', Sprite.prototype.recalculate)
|
||||
public animation: GetSet<string, this>;
|
||||
@getset('', Sprite.prototype.recalculate)
|
||||
public skin: GetSet<string, this>;
|
||||
@getset(null, Sprite.prototype.recalculate)
|
||||
public animation: GetSet<SpriteConfig['animation'], this>;
|
||||
@getset(null, Sprite.prototype.recalculate)
|
||||
public skin: GetSet<SpriteConfig['skin'], this>;
|
||||
@getset(null, Sprite.prototype.recalculate)
|
||||
public mask: GetSet<SpriteConfig['mask'], this>;
|
||||
@getset(false)
|
||||
public playing: GetSet<boolean, this>;
|
||||
public playing: GetSet<SpriteConfig['playing'], this>;
|
||||
@getset(10)
|
||||
public fps: AnimatedGetSet<number, this>;
|
||||
@getset('', Sprite.prototype.recalculate)
|
||||
public mask: GetSet<string, this>;
|
||||
@getset('', Sprite.prototype.recalculate)
|
||||
public maskBlend: AnimatedGetSet<number, this>;
|
||||
public fps: AnimatedGetSet<SpriteConfig['fps'], this>;
|
||||
@getset(0, Sprite.prototype.recalculate)
|
||||
public maskBlend: AnimatedGetSet<SpriteConfig['maskBlend'], this>;
|
||||
|
||||
private readonly animationData: SpriteData;
|
||||
private frame: FrameData = {
|
||||
private frame: SpriteData = {
|
||||
height: 0,
|
||||
width: 0,
|
||||
url: '',
|
||||
@@ -62,16 +55,14 @@ export class Sprite extends LayoutShape {
|
||||
private imageData: ImageData;
|
||||
private readonly computeCanvas: HTMLCanvasElement;
|
||||
|
||||
public get context(): CanvasRenderingContext2D {
|
||||
return this.computeCanvas.getContext('2d');
|
||||
}
|
||||
private readonly context: CanvasRenderingContext2D;
|
||||
|
||||
constructor(config?: SpriteConfig) {
|
||||
super(config);
|
||||
this.animationData = config.animationData;
|
||||
this.computeCanvas = Util.createCanvasElement();
|
||||
this.computeCanvas.width = COMPUTE_CANVAS_SIZE;
|
||||
this.computeCanvas.height = COMPUTE_CANVAS_SIZE;
|
||||
this.context = this.computeCanvas.getContext('2d');
|
||||
|
||||
this.recalculate();
|
||||
}
|
||||
@@ -95,14 +86,14 @@ export class Sprite extends LayoutShape {
|
||||
}
|
||||
|
||||
private recalculate() {
|
||||
const skin = this.animationData?.skins[this.skin()];
|
||||
const animation = this.animationData?.animations[this.animation()];
|
||||
const mask = this.animationData?.skins[this.mask()];
|
||||
const skin = this.skin();
|
||||
const animation = this.animation();
|
||||
const mask = this.mask();
|
||||
const blend = this.maskBlend();
|
||||
if (!animation || animation.frames.length === 0) return;
|
||||
if (!this.context || !animation || animation.length === 0) return;
|
||||
|
||||
this.frameId %= animation.frames.length;
|
||||
this.frame = animation.frames[this.frameId];
|
||||
this.frameId %= animation.length;
|
||||
this.frame = animation[this.frameId];
|
||||
this.offset(this.getOriginOffset());
|
||||
|
||||
this.imageData = this.context.createImageData(
|
||||
|
||||
12
src/global.d.ts
vendored
12
src/global.d.ts
vendored
@@ -1,9 +1,19 @@
|
||||
declare module "*.png" {
|
||||
const value: any;
|
||||
const value: import('./components/Sprite').SpriteData;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module "*.glsl" {
|
||||
const value: string;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module "*.label" {
|
||||
const value: Record<string, number>;
|
||||
export = value;
|
||||
}
|
||||
|
||||
declare module "*.anim" {
|
||||
const value: import('./components/Sprite').SpriteData[];
|
||||
export = value;
|
||||
}
|
||||
29
tools/loaders/animation-loader.js
Normal file
29
tools/loaders/animation-loader.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const path = require('path');
|
||||
const {readdirSync} = require('fs');
|
||||
const loadImage = require('../utils/load-image');
|
||||
const nameRegex = /([^\d]*)\d+\.png$/;
|
||||
|
||||
module.exports = function () {
|
||||
const callback = this.async();
|
||||
const directoryPath = path.dirname(this.resourcePath);
|
||||
const files = readdirSync(directoryPath)
|
||||
.filter(file => nameRegex.test(file))
|
||||
.map(file => path.resolve(directoryPath, file));
|
||||
|
||||
files.forEach(file => this.addDependency(file));
|
||||
|
||||
loadAnimation(files)
|
||||
.then(result => callback(null, result))
|
||||
.catch(error => callback(error));
|
||||
};
|
||||
|
||||
async function loadAnimation(files) {
|
||||
const frames = [];
|
||||
for (const file of files) {
|
||||
frames.push(await loadImage(file));
|
||||
}
|
||||
|
||||
return `export default ${JSON.stringify(frames)};`;
|
||||
}
|
||||
|
||||
|
||||
24
tools/loaders/label-loader.js
Normal file
24
tools/loaders/label-loader.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = function (source) {
|
||||
const json = {};
|
||||
source.split(/\r?\n/).forEach(line => {
|
||||
if (!line) return;
|
||||
const parts = line.split('\t');
|
||||
json[parts[2]] = parseFloat(parts[0]);
|
||||
});
|
||||
|
||||
return `
|
||||
const json = ${JSON.stringify(json)};
|
||||
const proxy = new Proxy(json, {
|
||||
get(target, prop) {
|
||||
if (target[prop]) {
|
||||
return target[prop];
|
||||
} else {
|
||||
console.warn('Missing label:', prop);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default proxy;
|
||||
`;
|
||||
};
|
||||
8
tools/loaders/sprite-loader.js
Normal file
8
tools/loaders/sprite-loader.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const loadImage = require('../utils/load-image');
|
||||
|
||||
module.exports = function () {
|
||||
const callback = this.async();
|
||||
loadImage(this.resourcePath)
|
||||
.then(sprite => callback(null, `export default ${JSON.stringify(sprite)}`))
|
||||
.catch(error => callback(error));
|
||||
};
|
||||
@@ -24,22 +24,38 @@ const compiler = webpack({
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/i,
|
||||
test: /\.glsl$/i,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.label$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 8192,
|
||||
},
|
||||
loader: 'label-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.glsl$/i,
|
||||
type: 'asset/source',
|
||||
test: /\.anim$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'animation-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.png$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'sprite-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolveLoader: {
|
||||
modules: ['node_modules', path.resolve(__dirname, './loaders')]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
alias: {
|
||||
@@ -64,7 +80,13 @@ const server = new WebpackDevServer(
|
||||
static: path.resolve(__dirname, '../public'),
|
||||
compress: true,
|
||||
port: 9000,
|
||||
open: true,
|
||||
setupMiddlewares: (middlewares, devServer) => {
|
||||
devServer.app.get('/test', (_, response) => {
|
||||
response.send('test?');
|
||||
});
|
||||
|
||||
return middlewares;
|
||||
}
|
||||
},
|
||||
compiler,
|
||||
);
|
||||
|
||||
30
tools/utils/load-image.js
Normal file
30
tools/utils/load-image.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const {promises: fs} = require('fs');
|
||||
const sizeOf = require('image-size');
|
||||
const nodeCanvas = require('canvas');
|
||||
|
||||
const SIZE = 1024;
|
||||
const context = nodeCanvas.createCanvas(SIZE, SIZE).getContext('2d');
|
||||
const image = new nodeCanvas.Image();
|
||||
|
||||
module.exports = async function (fileName) {
|
||||
const data = await fs.readFile(fileName, 'base64');
|
||||
const dimensions = sizeOf(fileName);
|
||||
|
||||
image.src = `data:image/png;base64,${data}`;
|
||||
context.clearRect(0, 0, SIZE, SIZE);
|
||||
context.drawImage(image, 0, 0, dimensions.width, dimensions.height);
|
||||
const imageData = context.getImageData(
|
||||
0,
|
||||
0,
|
||||
dimensions.width,
|
||||
dimensions.height,
|
||||
);
|
||||
|
||||
return {
|
||||
fileName,
|
||||
data: Array.from(imageData.data),
|
||||
url: `data:image/png;base64,${data}`,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user