前言

先说下正确打开方式是用JS+CSS3实现,例如baguettebox,只需要几行代码,且兼容性极高,动效极流畅。我这里用Canvas实现纯属瞎折腾,不但要从零开始,代码量大复杂度高还费CPU资源,低兼容性。



折腾


先放最终效果和最终代码。

最终源码
https://gitee.com/tczmh/canvas-album

在线演示
https://tczmh.gitee.io/canvas-album


然后讲讲折腾过程



先搞个空白HTML

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  7. <title>album-step1</title>
  8. <style>
  9. html, body {
  10. width: 100%;
  11. height: 100%;
  12. margin: 0;
  13. padding: 0;
  14. }
  15. body {
  16. overflow: hidden;
  17. }
  18. canvas {
  19. width: 100%;
  20. height: 100%;
  21. background: #333;
  22. }
  23. .toolbar {
  24. position: fixed;
  25. bottom: 1rem;
  26. left: 0;
  27. right: 0;
  28. text-align: center;
  29. }
  30. button {
  31. font-size: large;
  32. padding: .7rem 2.2rem;
  33. margin: 0 1rem;
  34. }
  35. </style>
  36. </head>
  37. <body>
  38. <canvas></canvas>
  39. <div class="toolbar">
  40. <button onclick="prev()">上一张</button>
  41. <button onclick="next()">下一张</button>
  42. </div>
  43. </body>
  44. </html>



实现基础的图片展示功能

  1. <script>
  2. const images = [
  3. 'images/1.jpg',
  4. 'images/2.png',
  5. 'images/3.jpg',
  6. 'images/4.jpg',
  7. 'images/5.jpg',
  8. 'images/6.jpg',
  9. 'images/7.jpg',
  10. 'images/8.jpg',
  11. ]
  12. const canvas = document.querySelector('canvas');
  13. const ctx = canvas.getContext('2d');
  14. const image = new Image();
  15. image.src = images[0];
  16. image.onload = function () {
  17. ctx.drawImage(image, 0, 0);
  18. }
  19. </script>


原图大概长这样


页面展示效果长这样


原因是原图分辨率是4000x2828 显示器分辨率不够,只展示了一个角,所以这里需要用到缩放,且缩放存在比例问题,不能粗暴的获取显示区域高宽,否则会拉伸,需要等比缩放且计算长宽边,最后在上下或者左右显示黑边。



等比缩放

  1. <script>
  2. const images = [
  3. 'images/1.jpg',
  4. 'images/2.png',
  5. 'images/3.jpg',
  6. 'images/4.jpg',
  7. 'images/5.jpg',
  8. 'images/6.jpg',
  9. 'images/7.jpg',
  10. 'images/8.jpg',
  11. ]
  12. const canvas = document.querySelector('canvas');
  13. const ctx = canvas.getContext('2d');
  14. const image = new Image();
  15. image.src = images[0];
  16. image.onload = function () {
  17. const size = resize();
  18. ctx.drawImage(image, size.dx, size.dy, size.width, size.height);
  19. }
  20. /** 根据显示区域分辨率和原图片分辨率,计算图片缩放后左边和宽高,这段代码只是随手写的,还有很大优化空间,仅做示范懒得修改了 */
  21. function resize() {
  22. canvas.width = window.innerWidth;
  23. canvas.height = window.innerHeight;
  24. let dx = 0;
  25. let dy = 0;
  26. let width = image.naturalWidth;
  27. let height = image.naturalHeight;
  28. if (canvas.width / canvas.height <= width / height) {
  29. height = height / width * canvas.width;
  30. width = canvas.width;
  31. dy = (canvas.height - height) / 2
  32. } else {
  33. width = width / height * canvas.height;
  34. height = canvas.height;
  35. dx = (canvas.width - width) / 2
  36. }
  37. return {
  38. dx: dx,
  39. dy: dy,
  40. width: width,
  41. height: height
  42. }
  43. }
  44. </script>


长屏效果和宽屏效果如下




自适应分辨率和图片切换

自适应分辨率需要做2步,上一段落实现了第一步,不同高宽比的显示区域,可以自适应显示锁定比例的图片,但由于只渲染一次,如果用户改变windows窗口大小,内容不会跟着改变,如果需要实现跟着窗口实时改变,则需要监听onresize方法,并实时渲染。也就是本段落实现的第二步。由于图片切换也很简单在这段一起实现了。

  1. <script>
  2. let index = 0;
  3. const images = [
  4. 'images/1.jpg',
  5. 'images/2.png',
  6. 'images/3.jpg',
  7. 'images/4.jpg',
  8. 'images/5.jpg',
  9. 'images/6.jpg',
  10. 'images/7.jpg',
  11. 'images/8.jpg',
  12. ]
  13. const canvas = document.querySelector('canvas');
  14. const ctx = canvas.getContext('2d');
  15. const image = new Image();
  16. function init() {
  17. load(function () {
  18. draw(image, resize(image));
  19. });
  20. }
  21. function load(callback) {
  22. image.src = images[index];
  23. image.onload = function () {
  24. callback();
  25. }
  26. }
  27. function draw(image, size) {
  28. ctx.drawImage(image, size.dx, size.dy, size.width, size.height);
  29. }
  30. /** 根据显示区域分辨率和原图片分辨率,计算图片缩放后左边和宽高,这段代码只是随手写的,还有很大优化空间,仅做示范懒得修改了 */
  31. function resize(image) {
  32. canvas.width = window.innerWidth;
  33. canvas.height = window.innerHeight;
  34. let dx = 0;
  35. let dy = 0;
  36. let width = image.naturalWidth;
  37. let height = image.naturalHeight;
  38. if (canvas.width / canvas.height <= width / height) {
  39. height = height / width * canvas.width;
  40. width = canvas.width;
  41. dy = (canvas.height - height) / 2
  42. } else {
  43. width = width / height * canvas.height;
  44. height = canvas.height;
  45. dx = (canvas.width - width) / 2
  46. }
  47. return {
  48. dx: dx,
  49. dy: dy,
  50. width: width,
  51. height: height
  52. }
  53. }
  54. /** 后期还需要加入防超出范围限制 */
  55. function next() {
  56. index++;
  57. init();
  58. }
  59. /** 后期还需要加入防超出范围限制 */
  60. function prev() {
  61. index--;
  62. init();
  63. }
  64. window.onload = function () {
  65. // 加载第一张图片
  66. init();
  67. }
  68. window.onresize = function (){
  69. // 使用缓存数据避免加载延迟导致黑屏闪屏
  70. draw(image,resize(image));
  71. }
  72. </script>

关键代码就是onresize,窗口改变大小后,会立刻执行draw方法,这里需要注意onresize会在瞬间反复被调用,image如果每次都去服务器请求,会卡成狗,这里必须把image对象缓存,并每次用变量中缓存的对象。才能实现纵享丝滑。


自适应窗口效果


图片切换太简单了不多介绍略过
图片切换效果



最后简单实现一下切换动画

截止到上一步,切换是直接替换图片,下一步是希望做到和手机相册类似的切换有图片滑入滑出效果。

切换动画示意图




即将用到一个核心方法 requestAnimationFrame 类似于Java的递归,如果动画没完成就接着用requestAnimationFrame 调用动画方法本身

  1. <script>
  2. let index = 0;
  3. const speed = 25;
  4. const images = [
  5. 'images/1.jpg',
  6. 'images/2.png',
  7. 'images/3.jpg',
  8. 'images/4.jpg',
  9. 'images/5.jpg',
  10. 'images/6.jpg',
  11. 'images/7.jpg',
  12. 'images/8.jpg',
  13. ]
  14. const canvas = document.querySelector('canvas');
  15. const ctx = canvas.getContext('2d');
  16. let temp = new Image();
  17. function init() {
  18. load(function (image) {
  19. draw(image, resize(image));
  20. temp = image;
  21. });
  22. }
  23. function load(callback) {
  24. const image = new Image();
  25. image.src = images[index];
  26. image.onload = function () {
  27. callback(image);
  28. }
  29. }
  30. function draw(image, size) {
  31. ctx.drawImage(image, size.dx, size.dy, size.width, size.height);
  32. }
  33. /** 根据显示区域分辨率和原图片分辨率,计算图片缩放后左边和宽高,这段代码只是随手写的,还有很大优化空间,仅做示范懒得修改了 */
  34. function resize(image) {
  35. canvas.width = window.innerWidth;
  36. canvas.height = window.innerHeight;
  37. let dx = 0;
  38. let dy = 0;
  39. let width = image.naturalWidth;
  40. let height = image.naturalHeight;
  41. if (canvas.width / canvas.height <= width / height) {
  42. height = height / width * canvas.width;
  43. width = canvas.width;
  44. dy = (canvas.height - height) / 2
  45. } else {
  46. width = width / height * canvas.height;
  47. height = canvas.height;
  48. dx = (canvas.width - width) / 2
  49. }
  50. return {
  51. dx: dx,
  52. dy: dy,
  53. width: width,
  54. height: height
  55. }
  56. }
  57. /** 动画部分代码重点你看这里 这个功能写起来累,只先写一个简单示范,代码有大量需要优化的地方,自行判断 */
  58. function next() {
  59. index++;
  60. // init();
  61. load(function (image) {
  62. const size1 = resize(temp);
  63. const size2 = resize(image);
  64. const end = size2.dx;
  65. // size1是旧图,起始位置不变,慢慢滑出屏幕
  66. // size2是新图,默认从屏幕外滑入,起始位置是原来dx再加一个屏幕宽度
  67. // end为最终位置
  68. size2.dx += window.innerWidth;
  69. animation(image, size1, size2, end);
  70. });
  71. }
  72. /** 上一张的动画懒得写了,累了 */
  73. function prev() {
  74. index--;
  75. init();
  76. }
  77. /** 这个功能写起来累,只先写一个简单示范 */
  78. function animation(image, size1, size2, end) {
  79. // 这里思路是temp作为上一张图,传入的image作为下一张图,目前只考虑下一张按钮
  80. // 新图从右侧屏幕外开始进入,旧图从屏幕中间向左滑出屏幕
  81. // 开始之前清屏,避免上次动画留下什么残影
  82. ctx.clearRect(0, 0, canvas.width, canvas.height);
  83. draw(temp, size1);
  84. draw(image, size2);
  85. size1.dx -= speed;
  86. size2.dx -= speed;
  87. if (size2.dx > end) {
  88. // 继续动画
  89. requestAnimationFrame(function () {
  90. // 层层递归
  91. animation(image, size1, size2, end);
  92. });
  93. } else {
  94. // 最后结尾肯定是 draw image
  95. size2.dx = end;
  96. // 开始之前清屏,避免上次动画留下什么残影
  97. ctx.clearRect(0, 0, canvas.width, canvas.height);
  98. draw(image, size2);
  99. // 由于requestAnimationFrame存在线程问题,必须确认最后一次执行才赋image给temp缓存
  100. temp = image;
  101. }
  102. }
  103. window.onload = function () {
  104. // 加载第一张图片
  105. init();
  106. }
  107. window.onresize = function () {
  108. // 使用缓存数据避免加载延迟导致黑屏闪屏
  109. draw(temp, resize(temp));
  110. }
  111. </script>


效果演示


我这里特地把speed调到50,目的是能看清楚效果。。。
另外还有几个地方存在改进空间,我只是写个demo就懒得调了
1. speed不能匀速,否则看着感觉很傻,凭感觉应该是先快后慢会舒服点
2. 画面放大后放慢速度后,会感觉卡顿,目前还没找到原因,可能和渲染次数、浏览器性能有关,目前还没有解决思路,待定



最终打死不改版

比上一段落优化了这几个地方
1. 加入了2个方向的切换效果
2. 加入下标判断防止越界
3. 加快动画速度,加入简单的先慢后快效果

还没解决的剩下卡顿问题,心累了,以后再说

完整源码如下


  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  7. <title>album-step1</title>
  8. <style>
  9. html, body {
  10. width: 100%;
  11. height: 100%;
  12. margin: 0;
  13. padding: 0;
  14. }
  15. body {
  16. overflow: hidden;
  17. }
  18. canvas {
  19. width: 100%;
  20. height: 100%;
  21. background: #333;
  22. }
  23. .toolbar {
  24. position: fixed;
  25. bottom: 1rem;
  26. left: 0;
  27. right: 0;
  28. text-align: center;
  29. }
  30. button {
  31. font-size: large;
  32. padding: .7rem 2.2rem;
  33. margin: 0 1rem;
  34. }
  35. </style>
  36. </head>
  37. <body>
  38. <canvas></canvas>
  39. <div class="toolbar">
  40. <button onclick="prev()">上一张</button>
  41. <button onclick="next()">下一张</button>
  42. </div>
  43. <script>
  44. let index = 0;
  45. /** 最后加个方向调整2个方向的差异 */
  46. let orientation = -1;
  47. let factor = 1;
  48. const speed = 50;
  49. const images = [
  50. 'images/1.jpg',
  51. 'images/2.png',
  52. 'images/3.jpg',
  53. 'images/4.jpg',
  54. 'images/5.jpg',
  55. 'images/6.jpg',
  56. 'images/7.jpg',
  57. 'images/8.jpg',
  58. ]
  59. const canvas = document.querySelector('canvas');
  60. const ctx = canvas.getContext('2d');
  61. let temp = new Image();
  62. function init() {
  63. load(function (image) {
  64. draw(image, resize(image));
  65. temp = image;
  66. });
  67. }
  68. function load(callback) {
  69. const image = new Image();
  70. image.src = images[index];
  71. image.onload = function () {
  72. callback(image);
  73. }
  74. }
  75. function draw(image, size) {
  76. ctx.drawImage(image, size.dx, size.dy, size.width, size.height);
  77. }
  78. /** 根据显示区域分辨率和原图片分辨率,计算图片缩放后左边和宽高,这段代码只是随手写的,还有很大优化空间,仅做示范懒得修改了 */
  79. function resize(image) {
  80. canvas.width = window.innerWidth;
  81. canvas.height = window.innerHeight;
  82. let dx = 0;
  83. let dy = 0;
  84. let width = image.naturalWidth;
  85. let height = image.naturalHeight;
  86. if (canvas.width / canvas.height <= width / height) {
  87. height = height / width * canvas.width;
  88. width = canvas.width;
  89. dy = (canvas.height - height) / 2
  90. } else {
  91. width = width / height * canvas.height;
  92. height = canvas.height;
  93. dx = (canvas.width - width) / 2
  94. }
  95. return {
  96. dx: dx,
  97. dy: dy,
  98. width: width,
  99. height: height
  100. }
  101. }
  102. /** 算了累就累了 一起写了 */
  103. function next() {
  104. // 加入下标判断 防止越界
  105. if (index == images.length - 1) {
  106. alert('已经是最后一张了')
  107. } else if (index < images.length - 1) {
  108. orientation = -1;
  109. factor = 0.7;
  110. index++;
  111. // init();
  112. load(function (image) {
  113. const size1 = resize(temp);
  114. const size2 = resize(image);
  115. const end = size2.dx;
  116. // size1是旧图,起始位置不变,慢慢滑出屏幕
  117. // size2是新图,默认从屏幕外滑入,起始位置是原来dx再加一个屏幕宽度
  118. // end为最终位置
  119. size2.dx += window.innerWidth;
  120. animation(image, size1, size2, end);
  121. });
  122. }
  123. }
  124. /** 算了累就累了 一起写了 */
  125. function prev() {
  126. // 加入下标判断 防止越界
  127. if (index == 0) {
  128. alert('已经是第一张了')
  129. } else if (index > 0) {
  130. orientation = 1;
  131. factor = 0.7;
  132. index--;
  133. // init();
  134. load(function (image) {
  135. const size1 = resize(temp);
  136. const size2 = resize(image);
  137. const end = size2.dx;
  138. // size1是旧图,起始位置不变,慢慢滑出屏幕
  139. // size2是新图,默认从屏幕外滑入,起始位置是原来dx再加一个屏幕宽度
  140. // end为最终位置
  141. size2.dx -= window.innerWidth;
  142. animation(image, size1, size2, end);
  143. });
  144. }
  145. }
  146. /** 这个功能写起来累,只先写一个简单示范 */
  147. function animation(image, size1, size2, end) {
  148. // 这里思路是temp作为上一张图,传入的image作为下一张图,目前只考虑下一张按钮
  149. // 新图从右侧屏幕外开始进入,旧图从屏幕中间向左滑出屏幕
  150. // 开始之前清屏,避免上次动画留下什么残影
  151. ctx.clearRect(0, 0, canvas.width, canvas.height);
  152. draw(temp, size1);
  153. draw(image, size2);
  154. size1.dx += speed * orientation * factor;
  155. size2.dx += speed * orientation * factor;
  156. // 用于前慢后快的系数,这样写不准确,就意思一下,实际
  157. factor += 0.15;
  158. console.log(factor)
  159. if (orientation > 0 ? size2.dx < end : size2.dx > end) {
  160. // 继续动画
  161. requestAnimationFrame(function () {
  162. // 层层递归
  163. animation(image, size1, size2, end);
  164. });
  165. } else {
  166. // 最后结尾肯定是 draw image
  167. size2.dx = end;
  168. // 开始之前清屏,避免上次动画留下什么残影
  169. ctx.clearRect(0, 0, canvas.width, canvas.height);
  170. draw(image, size2);
  171. // 由于requestAnimationFrame存在线程问题,必须确认最后一次执行才赋image给temp缓存
  172. temp = image;
  173. }
  174. }
  175. window.onload = function () {
  176. // 加载第一张图片
  177. init();
  178. }
  179. window.onresize = function () {
  180. // 使用缓存数据避免加载延迟导致黑屏闪屏
  181. draw(temp, resize(temp));
  182. }
  183. </script>
  184. </body>
  185. </html>


需要注意:这里的先慢后快的代码,我只是意思一下加了一个factor,简单示意了一下,其实是不准确的,准确写法需要参考css的ease-in的贝塞尔曲线,如下图


这次不录gif演示效果了,需要看最终效果的,直接点链接看线上展示吧


最终源码
https://gitee.com/tczmh/canvas-album

在线演示
https://tczmh.gitee.io/canvas-album

END

事后反复测试发现卡顿是源于图片过大,每一帧都要重新加载图片渲染,导致占用资源太大卡顿。本来这动画如果是css实现就还好,canvas就特别吃资源,图片一大就必然受不了。第一反应想的是用canvas第一次加载成功后,把图片转成0.7质量webp的base64,分辨率取浏览器显示区域,再转回image继续渲染。理论上可以在不损失太多画质的前提下改善卡顿问题,but,这里面有太多异步操作,我前端水平有限,异步转同步还不会搞,于是曲线救国,图片手动转为1920宽度webp质量0.7,图均从1mb+下降到100kb+,瞬间就如丝般顺滑了,如果以后项目到线上,可以用cdn的图片处理来解决这个问题。
PS: 已更新到最终代码和在线演示