// 程序化动作帧生成器(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 }; })();