animation-gen.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. // 程序化动作帧生成器(provider 无关,本地 canvas 变换,无需 API Key)。
  2. // 依据基础形象为每个动作生成 8-16 帧序列(PNG data URL),供 pet-engine.js 的 playClip 播放。
  3. // 数据源必须是 data: URL 或同源资源;若为跨域远程图,canvas 会被污染导致 toDataURL 抛错,
  4. // 此时 Promise reject,前端静默回退 CSS 动画。
  5. (function () {
  6. 'use strict';
  7. var MAX_DIM = 256;
  8. // 每个状态 → 一段随时间 t∈[0,1) 的仿射变换。
  9. // dx/dy: 相对宽/高的位移比例;rot: 弧度;sx/sy: 缩放。
  10. var PRESETS = {
  11. 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 }; } },
  12. 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) }; } },
  13. 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) }; } },
  14. 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) }; } },
  15. 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) }; } },
  16. 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) }; } },
  17. 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) }; } },
  18. remind: { frames: 10, fps: 8, loop: true, tf: function (t) { return { rot: 0.060 * Math.sin(4 * Math.PI * t) }; } },
  19. 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) }; } },
  20. 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) }; } },
  21. 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) }; } },
  22. tired: { frames: 12, fps: 5, loop: true, tf: function (t) { return { dy: 0.020, rot: -0.020 * Math.sin(2 * Math.PI * t) }; } },
  23. 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) }; } },
  24. 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) }; } },
  25. 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)) }; } },
  26. custom: { frames: 12, fps: 8, loop: true, tf: function (t) { var s = Math.sin(2 * Math.PI * t); return { dy: -0.015 * s }; } }
  27. };
  28. var KNOWN_STATES = ['idle', 'happy', 'busy', 'worried', 'excited', 'sad', 'tired',
  29. 'remind', 'energetic', 'curious', 'watching', 'waitingForInteraction', 'miniGame',
  30. 'sleeping', 'celebrate', 'custom'];
  31. function presetFor(state) { return PRESETS[state] || PRESETS.idle; }
  32. function loadImage(src) {
  33. return new Promise(function (resolve, reject) {
  34. var img = new Image();
  35. img.onload = function () { resolve(img); };
  36. img.onerror = function () { reject(new Error('image load failed')); };
  37. img.src = src;
  38. });
  39. }
  40. function makeClip(img, preset) {
  41. var iw = img.naturalWidth || img.width || 120;
  42. var ih = img.naturalHeight || img.height || 140;
  43. var scale = Math.min(1, MAX_DIM / Math.max(iw, ih));
  44. var cw = Math.max(1, Math.round(iw * scale));
  45. var ch = Math.max(1, Math.round(ih * scale));
  46. var canvas = document.createElement('canvas');
  47. canvas.width = cw;
  48. canvas.height = ch;
  49. var ctx = canvas.getContext('2d');
  50. var frames = [];
  51. for (var i = 0; i < preset.frames; i++) {
  52. var tr = preset.tf(i / preset.frames) || {};
  53. ctx.clearRect(0, 0, cw, ch);
  54. ctx.save();
  55. ctx.translate(cw / 2 + (tr.dx || 0) * cw, ch / 2 + (tr.dy || 0) * ch);
  56. if (tr.rot) ctx.rotate(tr.rot);
  57. ctx.scale(tr.sx || 1, tr.sy || 1);
  58. ctx.drawImage(img, -cw / 2, -ch / 2, cw, ch);
  59. ctx.restore();
  60. frames.push(canvas.toDataURL('image/png'));
  61. }
  62. return {
  63. frames: frames,
  64. sprite: null,
  65. frame_width: cw,
  66. frame_height: ch,
  67. count: frames.length,
  68. fps: preset.fps,
  69. loop: preset.loop
  70. };
  71. }
  72. // 依据 config.actions(状态→动作名)为每个动作生成剪辑,按动作名为键。
  73. // 返回 Promise<{ [actionName]: ActionClip }>;canvas 被污染时 reject。
  74. function generateForConfig(config, src) {
  75. var actions = (config && config.actions) || {};
  76. var map = {}; // actionName -> state
  77. KNOWN_STATES.forEach(function (s) {
  78. var a = actions[s] || s;
  79. if (!(a in map)) map[a] = s;
  80. });
  81. Object.keys(actions).forEach(function (s) {
  82. var a = actions[s];
  83. if (a && !(a in map)) map[a] = s;
  84. });
  85. return loadImage(src).then(function (img) {
  86. var cache = {}; // state -> clip
  87. var out = {};
  88. Object.keys(map).forEach(function (actionName) {
  89. var state = map[actionName];
  90. var clip = cache[state] || (cache[state] = makeClip(img, presetFor(state)));
  91. out[actionName] = clip;
  92. });
  93. return out;
  94. });
  95. }
  96. window.cfcPetGen = { generateForConfig: generateForConfig, presets: PRESETS };
  97. })();