{"id":20057529,"url":"https://github.com/dragonir/paint-heat-map","last_synced_at":"2025-07-09T19:39:02.102Z","repository":{"id":50358224,"uuid":"479189531","full_name":"dragonir/paint-heat-map","owner":"dragonir","description":"paint-heat-map  https://dragonir.github.io/paint-heat-map/","archived":false,"fork":false,"pushed_at":"2022-08-03T11:26:10.000Z","size":54872,"stargazers_count":67,"open_issues_count":0,"forks_count":13,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-05-05T14:42:32.814Z","etag":null,"topics":["css","html","javascript"],"latest_commit_sha":null,"homepage":"https://dragonir.github.io/paint-heat-map/","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dragonir.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-08T00:21:13.000Z","updated_at":"2024-12-20T13:20:30.000Z","dependencies_parsed_at":"2022-08-12T21:10:55.920Z","dependency_job_id":null,"html_url":"https://github.com/dragonir/paint-heat-map","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dragonir/paint-heat-map","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dragonir%2Fpaint-heat-map","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dragonir%2Fpaint-heat-map/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dragonir%2Fpaint-heat-map/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dragonir%2Fpaint-heat-map/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dragonir","download_url":"https://codeload.github.com/dragonir/paint-heat-map/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dragonir%2Fpaint-heat-map/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264502493,"owners_count":23618619,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css","html","javascript"],"created_at":"2024-11-13T12:59:22.024Z","updated_at":"2025-07-09T19:39:02.032Z","avatar_url":"https://github.com/dragonir.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 使用前端技术实现静态图片局部流动效果 🌊\n\n![banner.gif](./assets/images/banner.gif)\n\n\u003e 声明：本文涉及图文和模型素材仅用于个人学习、研究和欣赏，请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。\n\n## 背景\n\n如果你有玩过 `🎮` `《王者荣耀》`、`《阴阳师》` 等手游，一定注意到过它的启动动画、皮肤立绘卡片等场景，经常采用**静态底图加局部液态流动**效果的简单动画，这些流动动画可能出现在缓缓流动的水流 `🌊`、迎风飘动的旗帜 `🎏`、游戏角色衣袖 `🧜‍♀️`、随着时间缓动的云、雨、雾天气效果 `⛅` 等。这种过渡效果不仅节省了开发全量动画的成本，而且使得游戏画面更加热血、冒险、奥德赛、高级，也更加容易吸引玩家氪金 `💰`。\n\n本文使用前端开发技术，结合 `SVG` 和  `CSS` 来实现类似的液化流动效果。本文包含的知识点主要包括：`mask-image` 遮罩、`feTurbulence` 和 `feDisplacementMap` 滤镜、`filter` 属性、`canvas` 绘制方法、`TimelineMax` 动画以及`input[type=file]` 本地图片资源加载等。\n\n## 效果\n\n先来看看实现效果，下面几个示例以及 `👆` 文章 `Banner` 图都是应用了由本文内容生成的液态流动动画效果。由于`GIF` 图压缩比较严重，动画效果看起来不是很流畅 `🙃`，大家不妨通过以下演示页面链接，亲自体验一下效果，生成自己的 `传说`、`典藏` 皮肤立绘吧 `😅`。\n\n\u003e `👁‍🗨` 在线体验：\u003chttps://dragonir.github.io/paint-heat-map/\u003e\n\n`🌀` **雾气扩散** `塞尔达传说：旷野之息`\n\n![sample_0](./assets/images/sample_0.gif)\n\n`💃` **衣袖飘动** `貂蝉：猫影幻舞`\n\n![sample_1](./assets/images/sample_1.gif)\n\n`🌅` **湖光波动**\n\n![sample_2](./assets/images/sample_2.gif)\n\n`🔠` **文字液化**\n\n![sample_3](./assets/images/sample_3.gif)\n\n\u003e `📌` ps：体验页面部署在 `Gitpage` 上传图片功能不是真正上传到服务器，而是只会加载到浏览器本地，页面不会获取任何信息，大家可以放心体验，不用担心隐私泄漏问题。\n\n## 实现\n\n页面主要由 `2` 部分构成，顶部用于加载图片 ，并且可以通过按住 `🖱` 鼠标划动的方式绘制热点路径，给图片添加流动效果；底部是控制区域，点击按钮 `🔘` **清除画布**，可以清除绘制的流动动画效果、点击按钮 `🔘` **切换图片**可以加载本地的图片。\n\n![step_0](./assets/images/step_0.png)\n\n\u003e `📌` 注意，还有一个隐形的功能，当你绘制完成时，可以点击 `🖱` **鼠标右键**，然后选择保存图片，保存的这张图片就是我们绘制流体动画路径的热点图，利用这张热点图，使用本文的 `CSS` 知识，就能把静态图片转化成动态图啦！\n\n### HTML 页面结构\n\n`#sketch` 元素主要是用于绘制和加载流动效果热点图的画板；`#button_container` 是页面底部的按钮控制区域；`svg` 元素用于利用其 `filter` 滤镜实现液态流动动画效果，包括 `feTurbulence` 和 `feDisplacementMap` 滤镜。\n\n```html\n\u003cmain id=\"sketch\"\u003e\n  \u003ccanvas id=\"canvas\" data-img=\"\"\u003e\u003c/canvas\u003e\n  \u003cdiv class=\"mask\"\u003e\n    \u003cdiv id=\"maskInner\" class=\"mask-inner\"\u003e\u003c/div\u003e\n  \u003c/div\u003e\n\u003c/main\u003e\n\u003csection class=\"button_container\"\u003e\n  \u003cbutton class=\"button\"\u003e清除画布\u003c/button\u003e\n  \u003cbutton class=\"button\"\u003e\u003cinput class=\"input\" type=\"file\" id=\"upload\"\u003e上传图片\u003c/button\u003e\n\u003c/section\u003e\n\u003csvg\u003e\n  \u003cfilter id=\"heat\" filterUnits=\"objectBoundingBox\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\"\u003e\n    \u003cfeTurbulence id=\"heatturb\" type=\"fractalNoise\" numOctaves=\"1\" seed=\"2\" /\u003e\n    \u003cfeDisplacementMap xChannelSelector=\"G\" yChannelSelector=\"B\" scale=\"22\" in=\"SourceGraphic\" /\u003e\n  \u003c/filter\u003e\n\u003c/svg\u003e\n```\n\n#### `💡` feTurbulence 和 feDisplacementMap\n\n* `feTurbulence`：滤镜利用 `Perlin` 噪声函数创建了一个图像，利用它可以实现人造纹理比如说云纹、大理石纹等模拟滤镜效果。\n* `feDisplacementMap`：映射置换滤镜，该滤镜用来自图像中从 `in2` 到空间的像素值置换图像从 `in` 到空间的像素值。即它可以改变元素和图形的像素位置，通过遍历原图形的所有像素点，`feDisplacementMap` 重新映射替换一个新的位置，形成一个新的图形。该滤镜在业界的主流应用是对图形进行形变，扭曲，液化。\n\n### CSS 样式\n\n接着看看样式的实现，`main` 元素作为主容器并将主图案作为背景图片；`canvas` 作为画布占据 `100%` 的空间位置；`.mask` 和 `.mask-inner` 用于生成如下图所示热点路径与背景图相溶的效果，这种效果是借助 `mask-image` 实现的。最后，为了生成动态流动效果，`.mask-inner` 通过 `filter: url(#heat)` 将前面生成的 `svg` 作为滤镜来源，后续即将在 `JavaScript` 中通过不间断修改 `svg` 滤镜的属性，来生成液态流动动画。\n\n```css\nmain {\n  position: relative;\n  background-image: url('bg.jpg');\n  background-size: cover;\n  background-position: 100% 50%;\n}\ncanvas {\n  opacity: 0;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n.mask {\n  display: none;\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  mask-mode: luminance;\n  mask-size: 100% 100%;\n  backdrop-filter: hard-light;\n  mask-image: url('mask.png');\n}\n.mask-inner {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background: url('bg.jpg') 0% 0% repeat;\n  background-size: cover;\n  background-position: 100% 50%;\n  filter: url(#heat);\n  mask-image: url('mask.png')\n}\n```\n\n![step_1](./assets/images/step_1.png)\n\n#### `💡` mask-image\n\n`mask-image` `CSS` 属性用于设置元素上遮罩层的图像。\n\n**语法**：\n\n```css\n// 默认值，透明的黑色图像层，也就是没有遮罩层。\nmask-image: none;\n// \u003cmask-source\u003e\u003cmask\u003e或CSS图像的url的值\nmask-image: url(masks.svg#mask1);\n// \u003cimage\u003e 图片作为遮罩层\nmask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent);\nmask-image: image(url(mask.png), skyblue);\n// 多个值\nmask-image: image(url(mask.png), skyblue), linear-gradient(rgba(0, 0, 0, 1.0), transparent);\n// 全局值\nmask-image: inherit;\nmask-image: initial;\nmask-image: unset;\n```\n\n**兼容性**：\n\n![caniuse](./assets/images/caniuse.png)\n\n\u003e `⚡` 此功能某些浏览器尚在开发中，需要使用浏览器前缀以兼容不同浏览器。\n\n### JavaScript 方法\n\n#### ① 绘制热点图\n\n监听鼠标移动和点击事件，在 `canvas` 上绘制波动路径热点。\n\n```js\nvar canvas = document.getElementById('canvas');\nvar ctx = canvas.getContext('2d');\nvar sketch = document.getElementById('sketch');\nvar sketchStyle = window.getComputedStyle(sketch);\nvar mouse = { x: 0, y: 0 };\n\ncanvas.width = parseInt(sketchStyle.getPropertyValue('width'));\ncanvas.height = parseInt(sketchStyle.getPropertyValue('height'));\ncanvas.addEventListener('mousemove', e =\u003e {\n  mouse.x = e.pageX - canvas.getBoundingClientRect().left;\n  mouse.y = e.pageY - canvas.getBoundingClientRect().top;\n}, false);\n\nctx.lineWidth = 40;\nctx.lineJoin = 'round';\nctx.lineCap = 'round';\nctx.strokeStyle = 'black';\n\ncanvas.addEventListener('mousedown', () =\u003e {\n  ctx.beginPath();\n  ctx.moveTo(mouse.x, mouse.y);\n  canvas.addEventListener('mousemove', onPaint, false);\n}, false);\n\ncanvas.addEventListener('mouseup', () =\u003e {\n  canvas.removeEventListener('mousemove', onPaint, false);\n}, false);\n\nvar onPaint = () =\u003e {\n  ctx.lineTo(mouse.x, mouse.y);\n  ctx.stroke();\n  var url = canvas.toDataURL();\n  document.querySelectorAll('div').forEach(item =\u003e {\n    item.style.cssText += `\n      display: initial;\n      -webkit-mask-image: url(${url});\n      mask-image: url(${url});\n    `;\n  });\n};\n```\n\n绘制完成后，可以在页面中**右键**保存生成的波动路径热点图，直接将绘制满意的热点图放到 `CSS` 中，就能给喜欢的图片添加局部波动效果了，下面这张图片就是本示例页面使用的波动的热点路径图。\n\n![mask](./assets/images/mask.png)\n\n#### ② 生成动画\n\n为了生成实时更新的波动效果，本文使用了 `TweenMax` 来通过改变 `feTurbulence` 的 `baseFrequency` 属性值来实现，使用其他动画库或使用 `requestAnimationFrame` 也是可以实现相同的功能。\n\n```js\nfeTurb = document.querySelector('#heatturb');\nvar timeline = new TimelineMax({\n  repeat: -1,\n  yoyo: true\n}),\ntimeline.add(\n  new TweenMax.to(feTurb, 8, {\n    onUpdate: () =\u003e {\n      var bfX = this.progress() * 0.01 + 0.025,\n        bfY = this.progress() * 0.003 + 0.01,\n        bfStr = bfX.toString() + ' ' + bfY.toString();\n      feTurb.setAttribute('baseFrequency', bfStr);\n    }\n  }),\n0);\n```\n\n#### ③ 清除画布\n\n点击清除画布按钮，可以清空已经绘制的波动路径，主要是通过清除页面元素 `mask-image` 的属性值以及清 `canvas` 画布来实现的。\n\n```js\nfunction clear() {\n  document.querySelectorAll('div').forEach(item =\u003e {\n    item.style.cssText += `\n      display: none;\n      -webkit-mask-image: none;\n      mask-image: none;\n    `;\n  });\n}\n\ndocument.querySelectorAll('.button').forEach(item =\u003e {\n  item.addEventListener('click', () =\u003e {\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    clear();\n  })\n});\n```\n\n#### ④ 切换图片\n\n点击切换图片，可以加载本地的一张图片作为绘制底图，该功能是通过 `input[type=file]` 来实现图片资源的获取，然后通过修改 `CSS` 将它设置成新的画布背景。\n\n```js\ndocument.getElementById('upload').onchange = function () {\n  var imageFile = this.files[0];\n  var newImg = window.URL.createObjectURL(imageFile);\n  clear();\n  document.getElementById('sketch').style.cssText += `\n    background: url(${newImg});\n    background-size: cover;\n    background-position: center;\n  `;\n  document.getElementById('maskInner').style.cssText += `\n    background: url(${newImg});\n    background-size: cover;\n    background-position: center;\n  `;\n};\n```\n\n到这里，全部功能都实现完毕了，大家赶快制作一张自己喜欢的 `史诗皮肤` 或 `奥德赛小游戏` 的启动页面吧 `🤣`。\n\n![banner.gif](./assets/images/banner.gif)\n\n\u003e `📥` 源码地址：[https://github.com/dragonir/paint-heat-map](https://github.com/dragonir/paint-heat-map)\n\n## 总结\n\n本文包含的新知识点主要包括：\n\n* `mask-image` 遮罩元素\n* `feTurbulence` 和 `feDisplacementMap` `svg`滤镜\n* `filter` 属性\n* `Canvas` 绘制方法\n* `TimelineMax` 动画\n* `input[type=file]` 本地图片资源加载\n\n\u003e 想了解其他前端知识或其他未在本文中详细描述的 `Web 3D` 开发技术相关知识，可阅读我往期的文章。**转载请注明原文地址和作者**。如果觉得文章对你有帮助，不要忘了**一键三连哦 👍**。\n\n## 附录\n\n* [我的3D专栏可以点击此链接访问 👈](https://juejin.cn/column/7049923956257587213)\n\n* [1]. [🌐 使用Three.js实现炫酷的赛博朋克风格3D数字地球大屏](https://juejin.cn/post/7124116814937718797#comment)\n* [2]. [🦊 Three.js 实现3D开放世界小游戏：阿狸的多元宇宙](https://juejin.cn/post/7081429595689320478)\n* [3]. [🔥 Three.js 火焰效果实现艾尔登法环动态logo](https://juejin.cn/post/7077726955528781832)\n* [4]. [🐼 Three.js 实现2022冬奥主题3D趣味页面，含冰墩墩](https://juejin.cn/post/7060292943608807460)\n* `...`\n\n* [1]. [📷 前端实现很哇塞的浏览器端扫码功能](https://juejin.cn/post/7018722520345870350)\n* [2]. [🌏 前端瓦片地图加载之塞尔达传说旷野之息](https://juejin.cn/post/7007432493569671182)\n* [3]. [😱 仅用CSS几步实现赛博朋克2077风格视觉效果](https://juejin.cn/post/6972759988632551460)\n* `...`\n\n## 参考\n\n* [1]. [https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/feTurbulence](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/feTurbulence)\n* [2]. [https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/feDisplacementMap](https://developer.mozilla.org/zh-CN/docs/Web/SVG/Element/feDisplacementMap)\n* [3]. [https://developer.mozilla.org/zh-CN/docs/Web/CSS/mask-image](https://developer.mozilla.org/zh-CN/docs/Web/CSS/mask-image)\n* [4]. [https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdragonir%2Fpaint-heat-map","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdragonir%2Fpaint-heat-map","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdragonir%2Fpaint-heat-map/lists"}