| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- // 程序化动作帧生成器(provider 无关,本地 canvas 变换,无需 API Key)。
- // 依据基础形象为每个动作生成 8-16 帧序列(PNG data URL),供 pet-engine.js 的 playClip 播放。
- // 数据源必须是 data: URL 或同源资源;若为跨域远程图,canvas 会被污染导致 toDataURL 抛错,
- // 此时 Promise reject,前端静默回退 CSS 动画。
- (function () {
- 'use strict';
- var MAX_DIM = 256;
- // 每个状态 → 一段随时间 t∈[0,1) 的仿射变换。
- // dx/dy: 相对宽/高的位移比例;rot: 弧度;sx/sy: 缩放。
- var PRESETS = {
- idle: { frames: 12, fps: 8, loop: true, tf: function (t) { var s = Math.sin(2 * Math.PI * t); return { dy: -0.015 * s, sx: 1 - 0.010 * s, sy: 1 + 0.020 * s }; } },
- happy: { frames: 12, fps: 14, loop: true, tf: function (t) { return { dy: -0.060 * Math.abs(Math.sin(2 * Math.PI * t)), rot: 0.030 * Math.sin(2 * Math.PI * t) }; } },
- excited: { frames: 14, fps: 16, loop: true, tf: function (t) { return { dy: -0.080 * Math.abs(Math.sin(2 * Math.PI * t)), rot: 0.050 * Math.sin(2 * Math.PI * t) }; } },
- celebrate: { frames: 16, fps: 16, loop: true, tf: function (t) { return { dy: -0.100 * Math.abs(Math.sin(Math.PI * t)), rot: 0.080 * Math.sin(2 * Math.PI * t) }; } },
- busy: { frames: 8, fps: 16, loop: true, tf: function (t) { return { dx: 0.012 * Math.sin(4 * Math.PI * t), rot: 0.020 * Math.sin(4 * Math.PI * t) }; } },
- miniGame: { frames: 12, fps: 14, loop: true, tf: function (t) { return { dy: -0.060 * Math.abs(Math.sin(2 * Math.PI * t)), rot: 0.050 * Math.sin(4 * Math.PI * t) }; } },
- waitingForInteraction: { frames: 14, fps: 10, loop: true, tf: function (t) { return { rot: 0.050 * Math.sin(2 * Math.PI * t), dx: 0.010 * Math.sin(2 * Math.PI * t) }; } },
- remind: { frames: 10, fps: 8, loop: true, tf: function (t) { return { rot: 0.060 * Math.sin(4 * Math.PI * t) }; } },
- sleeping: { frames: 16, fps: 6, loop: true, tf: function (t) { return { rot: 0.040, sy: 1 + 0.025 * Math.sin(2 * Math.PI * t), dy: 0.010 * Math.sin(2 * Math.PI * t) }; } },
- worried: { frames: 12, fps: 6, loop: true, tf: function (t) { return { dy: 0.015, sy: 1 - 0.020 * Math.abs(Math.sin(Math.PI * t)), rot: -0.030 * Math.sin(2 * Math.PI * t) }; } },
- sad: { frames: 12, fps: 6, loop: true, tf: function (t) { return { dy: 0.015, sy: 1 - 0.020 * Math.abs(Math.sin(Math.PI * t)), rot: -0.030 * Math.sin(2 * Math.PI * t) }; } },
- tired: { frames: 12, fps: 5, loop: true, tf: function (t) { return { dy: 0.020, rot: -0.020 * Math.sin(2 * Math.PI * t) }; } },
- energetic: { frames: 14, fps: 18, loop: true, tf: function (t) { return { dy: -0.070 * Math.abs(Math.sin(2 * Math.PI * t)), rot: 0.040 * Math.sin(2 * Math.PI * t) }; } },
- curious: { frames: 12, fps: 8, loop: true, tf: function (t) { return { rot: 0.070 * Math.sin(2 * Math.PI * t), dy: -0.020 * Math.sin(2 * Math.PI * t) }; } },
- watching: { frames: 12, fps: 6, loop: true, tf: function (t) { return { rot: 0.030 * Math.sin(2 * Math.PI * t), dy: -0.010 * Math.abs(Math.sin(2 * Math.PI * t)) }; } },
- custom: { frames: 12, fps: 8, loop: true, tf: function (t) { var s = Math.sin(2 * Math.PI * t); return { dy: -0.015 * s }; } }
- };
- var KNOWN_STATES = ['idle', 'happy', 'busy', 'worried', 'excited', 'sad', 'tired',
- 'remind', 'energetic', 'curious', 'watching', 'waitingForInteraction', 'miniGame',
- 'sleeping', 'celebrate', 'custom'];
- function presetFor(state) { return PRESETS[state] || PRESETS.idle; }
- function loadImage(src) {
- return new Promise(function (resolve, reject) {
- var img = new Image();
- img.onload = function () { resolve(img); };
- img.onerror = function () { reject(new Error('image load failed')); };
- img.src = src;
- });
- }
- function makeClip(img, preset) {
- var iw = img.naturalWidth || img.width || 120;
- var ih = img.naturalHeight || img.height || 140;
- var scale = Math.min(1, MAX_DIM / Math.max(iw, ih));
- var cw = Math.max(1, Math.round(iw * scale));
- var ch = Math.max(1, Math.round(ih * scale));
- var canvas = document.createElement('canvas');
- canvas.width = cw;
- canvas.height = ch;
- var ctx = canvas.getContext('2d');
- var frames = [];
- for (var i = 0; i < preset.frames; i++) {
- var tr = preset.tf(i / preset.frames) || {};
- ctx.clearRect(0, 0, cw, ch);
- ctx.save();
- ctx.translate(cw / 2 + (tr.dx || 0) * cw, ch / 2 + (tr.dy || 0) * ch);
- if (tr.rot) ctx.rotate(tr.rot);
- ctx.scale(tr.sx || 1, tr.sy || 1);
- ctx.drawImage(img, -cw / 2, -ch / 2, cw, ch);
- ctx.restore();
- frames.push(canvas.toDataURL('image/png'));
- }
- return {
- frames: frames,
- sprite: null,
- frame_width: cw,
- frame_height: ch,
- count: frames.length,
- fps: preset.fps,
- loop: preset.loop
- };
- }
- // 依据 config.actions(状态→动作名)为每个动作生成剪辑,按动作名为键。
- // 返回 Promise<{ [actionName]: ActionClip }>;canvas 被污染时 reject。
- function generateForConfig(config, src) {
- var actions = (config && config.actions) || {};
- var map = {}; // actionName -> state
- KNOWN_STATES.forEach(function (s) {
- var a = actions[s] || s;
- if (!(a in map)) map[a] = s;
- });
- Object.keys(actions).forEach(function (s) {
- var a = actions[s];
- if (a && !(a in map)) map[a] = s;
- });
- return loadImage(src).then(function (img) {
- var cache = {}; // state -> clip
- var out = {};
- Object.keys(map).forEach(function (actionName) {
- var state = map[actionName];
- var clip = cache[state] || (cache[state] = makeClip(img, presetFor(state)));
- out[actionName] = clip;
- });
- return out;
- });
- }
- window.cfcPetGen = { generateForConfig: generateForConfig, presets: PRESETS };
- })();
|