index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. // index.js
  2. // 获取应用实例
  3. import { VueLikePage } from "../../utils/page";
  4. import { CDN_URL, API_BASE_URL, VIDEO_BASE_URL, app } from "../../config/index";
  5. VueLikePage([], {
  6. data: {
  7. cdn_url: "",
  8. baseUrl: API_BASE_URL + "/",
  9. url_link: "",
  10. id: "1",
  11. type: "",
  12. loadCompele: false,
  13. filePath: "",
  14. projectid: '',
  15. isEditing: false,
  16. info: {
  17. resourceImg: {},
  18. banner: {},
  19. sceneTitleImg: {},
  20. recordTitleImg: {},
  21. rescan: {},
  22. activeSceneBdImg: {},
  23. },
  24. isZoom: false,
  25. ipsImgList: [],
  26. selectedIp: null,
  27. selectedIpIndex: -1,
  28. ipScaleX: 1,
  29. ipScaleY: 1,
  30. ipRotate: 0,
  31. ipConfirmed: false,
  32. ipLeft: 0,
  33. ipTop: 0,
  34. positionInitialized: false,
  35. canvasWidth: 375,
  36. canvasHeight: 600,
  37. widgetVisible: true,
  38. zoomScrollLeft: 0,
  39. scaleOrientation: ''
  40. },
  41. methods: {
  42. zoom() {
  43. this.setData({
  44. isZoom: !this.data.isZoom,
  45. selectedIp: null,
  46. selectedIpIndex: -1,
  47. ipScaleX: 1,
  48. ipScaleY: 1,
  49. ipRotate: 0,
  50. ipConfirmed: false,
  51. });
  52. },
  53. onLoad: function (options) {
  54. let { rdw, id, type, projectid } = options;
  55. if(!projectid){
  56. projectid = 'ZHS2409020-1';
  57. }
  58. this.initIpsList(projectid);
  59. this.getData(projectid);
  60. let link = "";
  61. if (type == "0") {
  62. link = `${VIDEO_BASE_URL}4dvedio/${rdw}.mp4`;
  63. } else {
  64. link = `${VIDEO_BASE_URL}4dpic/${rdw}.jpg`;
  65. // link='https://4dkk.4dage.com/fusion/test/file/797098a37e0c4588ae88f2ec4b1f12df.png'
  66. }
  67. this.setData({
  68. url_link: link,
  69. id,
  70. type,
  71. projectid,
  72. cdn_url: CDN_URL + "/" + projectid,
  73. });
  74. this.downloadF((data) => {
  75. this.setData({
  76. filePath: data,
  77. });
  78. });
  79. },
  80. initIpsList(projectid) {
  81. const list = [];
  82. for (let i = 1; i <= 42; i++) {
  83. list.push({
  84. name: String(i),
  85. imgUrl: `${CDN_URL}/${projectid}/ip/${i}.png`,
  86. });
  87. }
  88. this.setData({
  89. ipsImgList: list,
  90. });
  91. },
  92. loadcompele() {
  93. this.setData({
  94. loadCompele: true,
  95. });
  96. },
  97. cancel() {
  98. // wx.reLaunch({
  99. // url: "/pages/work/index",
  100. // });
  101. wx.navigateBack();
  102. },
  103. edit() {
  104. this.setData({
  105. isEditing: !this.data.isEditing,
  106. });
  107. },
  108. downloadF(cb = () => {}) {
  109. let link = this.data.url_link,
  110. m_type = "";
  111. if (this.data.type == "1") {
  112. m_type = "jpeg";
  113. } else {
  114. m_type = "video";
  115. }
  116. wx.downloadFile({
  117. url: link,
  118. success: (res) => {
  119. if (res.statusCode == "404") {
  120. return app.showAlert("作品暂未生成,请稍后再试", () => {
  121. wx.navigateBack();
  122. });
  123. }
  124. //判断是否为数组
  125. let typeType =
  126. Object.prototype.toString.call(res.header["Content-Type"]) ==
  127. "[object String]"
  128. ? res.header["Content-Type"]
  129. : res.header["Content-Type"][0];
  130. //判断不是xml文件
  131. if (typeType.indexOf(m_type) > -1) {
  132. cb(res.tempFilePath);
  133. }
  134. },
  135. fail: () => {
  136. app.showAlert("作品暂未生成,请稍后再试");
  137. },
  138. });
  139. this.setData({
  140. showModal: false,
  141. });
  142. },
  143. async generateImage() {
  144. wx.showLoading({ title: '生成中...', mask: true });
  145. try {
  146. const query = wx.createSelectorQuery();
  147. query.select('.w_video').boundingClientRect();
  148. if (this.data.selectedIp) {
  149. query.select('.ip-overlay').boundingClientRect();
  150. }
  151. const res = await new Promise(resolve => query.exec(resolve));
  152. const container = res[0];
  153. const overlay = this.data.selectedIp ? res[1] : null;
  154. if (!container) throw new Error('Cannot find container');
  155. let width = container.width;
  156. let height = container.height;
  157. if (this.data.isZoom) {
  158. const imgRes = await new Promise((resolve, reject) => {
  159. wx.getImageInfo({
  160. src: this.data.url_link,
  161. success: resolve,
  162. fail: reject
  163. })
  164. });
  165. width = imgRes.width;
  166. height = imgRes.height;
  167. }
  168. // Limit canvas size to avoid incomplete rendering on some devices
  169. const dpr = wx.getSystemInfoSync().pixelRatio;
  170. const maxCanvasSize = 4096;
  171. let scale = 1;
  172. if (width * dpr > maxCanvasSize || height * dpr > maxCanvasSize) {
  173. scale = Math.min(maxCanvasSize / (width * dpr), maxCanvasSize / (height * dpr));
  174. }
  175. const canvasWidth = width * scale;
  176. const canvasHeight = height * scale;
  177. await this._resetWidget(canvasWidth, canvasHeight);
  178. const widget = this.selectComponent('#widget');
  179. const mainUrl = this.data.url_link;
  180. let wxml = '';
  181. if (this.data.isZoom) {
  182. wxml = `
  183. <view class="container">
  184. <image class="main" src="${mainUrl}"></image>
  185. </view>
  186. `;
  187. } else {
  188. const bgUrl = this.data.info.resourceImg.bg ? (this.data.cdn_url + this.data.info.resourceImg.bg) : '';
  189. wxml = `
  190. <view class="container">
  191. <image class="bg" src="${bgUrl}"></image>
  192. ${(this.data.type != '0') ? `<image class="main" src="${mainUrl}"></image>` : ''}
  193. </view>
  194. `;
  195. }
  196. const style = {
  197. container: { width: canvasWidth, height: canvasHeight, position: 'relative', overflow: 'hidden' },
  198. bg: { width: canvasWidth, height: canvasHeight, position: 'absolute', left: 0, top: 0 },
  199. main: { width: canvasWidth, height: canvasHeight, position: 'absolute', left: 0, top: 0 }
  200. };
  201. await widget.renderToCanvas({ wxml, style });
  202. if (this.data.selectedIp && overlay) {
  203. const ctx = widget.ctx;
  204. const use2d = widget.data.use2dCanvas;
  205. const stickerUrl = this.data.selectedIp.imgUrl;
  206. // Get local path
  207. const stickerInfo = await new Promise((resolve, reject) => {
  208. wx.getImageInfo({
  209. src: stickerUrl,
  210. success: resolve,
  211. fail: reject
  212. })
  213. });
  214. let imgToDraw = stickerInfo.path;
  215. if (use2d) {
  216. const canvas = widget.canvas;
  217. const img = canvas.createImage();
  218. await new Promise((resolve, reject) => {
  219. img.onload = resolve;
  220. img.onerror = reject;
  221. img.src = stickerInfo.path;
  222. });
  223. imgToDraw = img;
  224. }
  225. const ratio = canvasWidth / container.width;
  226. const cx = ((overlay.left - container.left + overlay.width / 2) + (this.data.isZoom ? this.data.zoomScrollLeft : 0)) * ratio;
  227. const cy = (overlay.top - container.top + overlay.height / 2) * ratio;
  228. const overlayWidth = overlay.width * ratio;
  229. const overlayHeight = overlay.height * ratio;
  230. ctx.save();
  231. ctx.translate(cx, cy);
  232. ctx.rotate(this.data.ipRotate * Math.PI / 180);
  233. ctx.scale(this.data.ipScaleX, this.data.ipScaleY);
  234. ctx.drawImage(imgToDraw, -overlayWidth / 2, -overlayHeight / 2, overlayWidth, overlayHeight);
  235. ctx.restore();
  236. // If legacy, might need draw()
  237. if (!use2d) {
  238. await new Promise(resolve => ctx.draw(true, resolve));
  239. }
  240. }
  241. const { tempFilePath } = await widget.canvasToTempFilePath();
  242. // Save
  243. wx.saveImageToPhotosAlbum({
  244. filePath: tempFilePath,
  245. success: () => {
  246. wx.showModal({
  247. title: "提示",
  248. content: "已保存到相册,快去分享吧",
  249. showCancel: false,
  250. });
  251. },
  252. fail: (e) => {
  253. if (!(e.errMsg.indexOf("cancel") > -1)) {
  254. wx.showModal({
  255. title: "提示",
  256. content: "保存失败,请检查是否开启相册保存权限",
  257. showCancel: false,
  258. });
  259. }
  260. }
  261. });
  262. } catch (e) {
  263. console.error(e);
  264. wx.showToast({ title: '生成失败', icon: 'none' });
  265. } finally {
  266. wx.hideLoading();
  267. }
  268. },
  269. async _resetWidget(width, height) {
  270. this.setData({ widgetVisible: false });
  271. await new Promise(r => setTimeout(r, 50));
  272. this.setData({ canvasWidth: width, canvasHeight: height, widgetVisible: true });
  273. await new Promise(r => setTimeout(r, 120));
  274. },
  275. onZoomScroll(e) {
  276. this.setData({ zoomScrollLeft: e.detail.scrollLeft || 0 });
  277. },
  278. saveAlbum() {
  279. let type = this.data.type;
  280. if (this.data.projectid == 'ZHS2409020-1') {
  281. if (this.data.selectedIp && !this.data.ipConfirmed) {
  282. wx.showToast({
  283. title: '请先确认标签',
  284. icon: 'none'
  285. })
  286. return;
  287. }
  288. this.generateImage();
  289. return;
  290. }
  291. wx.showLoading({
  292. title: "保存中…",
  293. mask: true,
  294. });
  295. if (this.data.filePath) {
  296. let api =
  297. type == "0" ? "saveVideoToPhotosAlbum" : "saveImageToPhotosAlbum";
  298. wx[api]({
  299. filePath: this.data.filePath,
  300. success() {
  301. wx.showModal({
  302. title: "提示",
  303. content: "已保存到相册,快去分享吧",
  304. showCancel: false,
  305. });
  306. },
  307. fail: (e) => {
  308. if (!(e.errMsg.indexOf("cancel") > -1)) {
  309. wx.showModal({
  310. title: "提示",
  311. content:
  312. "保存失败,请检查是否开启相册保存权限,可在「右上角」 - 「设置」里查看",
  313. showCancel: false,
  314. });
  315. }
  316. },
  317. complete: () => {
  318. wx.hideLoading();
  319. },
  320. });
  321. }
  322. },
  323. // 横琴是ZHS2409020-1,替换
  324. getData(prjId = "ZHS2305758-1") {
  325. wx.showLoading({
  326. title: "资源加载中",
  327. });
  328. this.setData({
  329. cdn_url: CDN_URL + "/" + prjId,
  330. });
  331. wx.request({
  332. url: `${VIDEO_BASE_URL}project/4dage-sxb/${prjId}/config.json`,
  333. success: ({ data: { title, ...rest } }) => {
  334. this.setData(
  335. {
  336. info: rest,
  337. },
  338. () => {
  339. wx.hideLoading();
  340. }
  341. );
  342. wx.setNavigationBarTitle({
  343. title: title,
  344. });
  345. },
  346. });
  347. },
  348. selectIp(e) {
  349. const index = e.currentTarget.dataset.index;
  350. const item = this.data.ipsImgList[index];
  351. if (!item) return;
  352. this.setData({
  353. selectedIpIndex: index,
  354. selectedIp: item,
  355. ipScaleX: 1,
  356. ipScaleY: 1,
  357. ipRotate: 0,
  358. ipConfirmed: false,
  359. positionInitialized: false,
  360. }, () => {
  361. this.getOverlayRect();
  362. // Initialize position
  363. const query = wx.createSelectorQuery();
  364. query.select('.w_video').boundingClientRect();
  365. query.select('.ip-overlay').boundingClientRect();
  366. query.exec((res) => {
  367. const container = res[0];
  368. const overlay = res[1];
  369. if (container && overlay) {
  370. this.setData({
  371. ipLeft: overlay.left - container.left,
  372. ipTop: overlay.top - container.top,
  373. positionInitialized: true
  374. });
  375. }
  376. });
  377. });
  378. },
  379. dragStart(e) {
  380. if (this.data.ipConfirmed) return;
  381. const touch = e.touches[0];
  382. this.setData({
  383. dragStartX: touch.clientX,
  384. dragStartY: touch.clientY,
  385. startIpLeft: this.data.ipLeft,
  386. startIpTop: this.data.ipTop
  387. });
  388. },
  389. dragMove(e) {
  390. if (this.data.ipConfirmed) return;
  391. const touch = e.touches[0];
  392. const dx = touch.clientX - this.data.dragStartX;
  393. const dy = touch.clientY - this.data.dragStartY;
  394. this.setData({
  395. ipLeft: this.data.startIpLeft + dx,
  396. ipTop: this.data.startIpTop + dy
  397. });
  398. },
  399. getOverlayRect() {
  400. const query = wx.createSelectorQuery();
  401. query.select('.ip-overlay').boundingClientRect(rect => {
  402. if (rect) {
  403. this.setData({
  404. centerX: rect.left + rect.width / 2,
  405. centerY: rect.top + rect.height / 2
  406. });
  407. }
  408. }).exec();
  409. },
  410. rotateStart(e) {
  411. this.getOverlayRect();
  412. const touch = e.touches[0];
  413. const dx = touch.clientX - this.data.centerX;
  414. const dy = touch.clientY - this.data.centerY;
  415. const startAngle = Math.atan2(dy, dx) * 180 / Math.PI;
  416. this.setData({
  417. startRotateAngle: startAngle,
  418. baseIpRotate: this.data.ipRotate
  419. });
  420. },
  421. rotateMove(e) {
  422. const touch = e.touches[0];
  423. const dx = touch.clientX - this.data.centerX;
  424. const dy = touch.clientY - this.data.centerY;
  425. const currentAngle = Math.atan2(dy, dx) * 180 / Math.PI;
  426. const diff = currentAngle - this.data.startRotateAngle;
  427. let nextRotate = this.data.baseIpRotate + diff;
  428. this.setData({
  429. ipRotate: nextRotate
  430. });
  431. },
  432. scaleStart(e) {
  433. const touch = e.touches[0];
  434. this.setData({
  435. startX: touch.clientX,
  436. startY: touch.clientY,
  437. baseIpScaleX: this.data.ipScaleX,
  438. baseIpScaleY: this.data.ipScaleY,
  439. scaleOrientation: ''
  440. });
  441. },
  442. scaleMove(e) {
  443. const touch = e.touches[0];
  444. const dx = touch.clientX - this.data.startX;
  445. const dy = touch.clientY - this.data.startY;
  446. const factor = 0.005;
  447. let { scaleOrientation } = this.data;
  448. if (!scaleOrientation) {
  449. const adx = Math.abs(dx);
  450. const ady = Math.abs(dy);
  451. if (adx > ady && adx > 3) {
  452. scaleOrientation = 'x';
  453. } else if (ady >= adx && ady > 3) {
  454. scaleOrientation = 'y';
  455. } else {
  456. return;
  457. }
  458. this.setData({ scaleOrientation });
  459. }
  460. if (scaleOrientation === 'x') {
  461. let nextScaleX = this.data.baseIpScaleX + (-dx) * factor;
  462. if (nextScaleX < 0.2) nextScaleX = 0.2;
  463. if (nextScaleX > 4) nextScaleX = 4;
  464. this.setData({ ipScaleX: nextScaleX });
  465. } else {
  466. let nextScaleY = this.data.baseIpScaleY + (-dy) * factor;
  467. if (nextScaleY < 0.2) nextScaleY = 0.2;
  468. if (nextScaleY > 4) nextScaleY = 4;
  469. this.setData({ ipScaleY: nextScaleY });
  470. }
  471. },
  472. rotateIp() {
  473. // 兼容旧的点击事件,如果不需要可以删除,但保留也不会出错
  474. if (!this.data.selectedIp) return;
  475. const nextRotate = (this.data.ipRotate + 15) % 360;
  476. this.setData({
  477. ipRotate: nextRotate,
  478. });
  479. },
  480. scaleIp() {
  481. if (!this.data.selectedIp) return;
  482. let nextScaleX = this.data.ipScaleX + 0.25;
  483. let nextScaleY = this.data.ipScaleY + 0.25;
  484. if (nextScaleX > 2.5 || nextScaleY > 2.5) {
  485. nextScaleX = 1;
  486. nextScaleY = 1;
  487. }
  488. this.setData({
  489. ipScaleX: nextScaleX,
  490. ipScaleY: nextScaleY,
  491. });
  492. },
  493. deleteIp() {
  494. this.setData({
  495. selectedIp: null,
  496. selectedIpIndex: -1,
  497. ipScaleX: 1,
  498. ipScaleY: 1,
  499. ipRotate: 0,
  500. ipConfirmed: false,
  501. });
  502. },
  503. confirmIp() {
  504. if (!this.data.selectedIp) return;
  505. this.setData({
  506. ipConfirmed: true,
  507. });
  508. },
  509. },
  510. });