index.js 17 KB

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