You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /**
  2. * @Author: Caven
  3. * @Date: 2021-01-18 17:46:40
  4. */
  5. class WindCanvas {
  6. constructor(ctx) {
  7. this.options = {}
  8. this.particles = []
  9. this.ctx = ctx
  10. this.animationLoop = undefined
  11. this.animate = this.animate.bind(this)
  12. }
  13. /**
  14. *
  15. * @param m
  16. * @param min
  17. * @param max
  18. * @param colorScale
  19. * @returns {number}
  20. * @private
  21. */
  22. _indexFor(m, min, max, colorScale) {
  23. return Math.max(
  24. 0,
  25. Math.min(
  26. colorScale.length - 1,
  27. Math.round(((m - min) / (max - min)) * (colorScale.length - 1))
  28. )
  29. )
  30. }
  31. /**
  32. *
  33. * @private
  34. */
  35. _moveParticles() {
  36. if (!this.particles || !this.particles.length) {
  37. return
  38. }
  39. let width = this.ctx.canvas.width
  40. let height = this.ctx.canvas.height
  41. let particles = this.particles
  42. let maxAge = this.options.maxAge
  43. let velocityScale =
  44. typeof this.options.velocityScale === 'function'
  45. ? this.options.velocityScale()
  46. : this.options.velocityScale
  47. for (let i = 0; i < particles.length; i++) {
  48. let particle = particles[i]
  49. if (particle.age > maxAge) {
  50. particle.age = 0
  51. this.field.randomize(particle, width, height, this.unProject)
  52. }
  53. let x = particle.x
  54. let y = particle.y
  55. let vector = this.field.interpolatedValueAt(x, y)
  56. if (vector === null) {
  57. particle.age = maxAge
  58. } else {
  59. let xt = x + vector.u * velocityScale
  60. let yt = y + vector.v * velocityScale
  61. if (this.field.hasValueAt(xt, yt)) {
  62. particle.xt = xt
  63. particle.yt = yt
  64. particle.m = vector.m
  65. } else {
  66. particle.x = xt
  67. particle.y = yt
  68. particle.age = maxAge
  69. }
  70. }
  71. particle.age++
  72. }
  73. }
  74. /**
  75. *
  76. * @private
  77. */
  78. _drawParticles() {
  79. if (!this.particles || !this.particles.length) {
  80. return
  81. }
  82. let particles = this.particles
  83. let prev = this.ctx.globalCompositeOperation
  84. this.ctx.globalCompositeOperation = 'destination-in'
  85. this.ctx.fillRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
  86. this.ctx.globalCompositeOperation = prev
  87. this.ctx.globalAlpha = this.options.globalAlpha
  88. this.ctx.fillStyle = 'rgba(0, 0, 0, ' + this.options.globalAlpha + ')'
  89. this.ctx.lineWidth = this.options.lineWidth ? this.options.lineWidth : 1
  90. this.ctx.strokeStyle = this.options.colorScale
  91. ? this.options.colorScale
  92. : '#fff'
  93. let i = 0
  94. let len = particles.length
  95. if (this.field && len > 0) {
  96. let min = void 0
  97. let max = void 0
  98. if (this.options.minVelocity && this.options.maxVelocity) {
  99. min = this.options.minVelocity
  100. max = this.options.maxVelocity
  101. } else {
  102. let _a = this.field.range
  103. min = _a[0]
  104. max = _a[1]
  105. }
  106. for (; i < len; i++) {
  107. this[
  108. this.options.useCoordsDraw
  109. ? '_drawCoordsParticle'
  110. : '_drawPixelParticle'
  111. ](particles[i], min, max)
  112. }
  113. }
  114. }
  115. /**
  116. *
  117. * @param particle
  118. * @param min
  119. * @param max
  120. */
  121. _drawPixelParticle(particle, min, max) {
  122. let pointPrev = [particle.x, particle.y]
  123. let pointNext = [particle.xt, particle.yt]
  124. let dx = particle.xt - particle.x
  125. let dy = particle.yt - particle.y
  126. if (dx * dx + dy * dy > 20 * 20) {
  127. return
  128. }
  129. if (
  130. pointNext &&
  131. pointPrev &&
  132. pointNext[0] &&
  133. pointNext[1] &&
  134. pointPrev[0] &&
  135. pointPrev[1] &&
  136. particle.age <= this.options.maxAge
  137. ) {
  138. this._drawStroke(pointPrev, pointNext, particle, min, max)
  139. }
  140. }
  141. /**
  142. *
  143. * @param particle
  144. * @param min
  145. * @param max
  146. */
  147. _drawCoordsParticle(particle, min, max) {
  148. let source = [particle.x, particle.y]
  149. let target = [particle.xt, particle.yt]
  150. if (
  151. target &&
  152. source &&
  153. target[0] &&
  154. target[1] &&
  155. source[0] &&
  156. source[1] &&
  157. this.intersectsCoordinate(target) &&
  158. particle.age <= this.options.maxAge
  159. ) {
  160. let pointPrev = this.project(source)
  161. let pointNext = this.project(target)
  162. this._drawStroke(pointPrev, pointNext, particle, min, max)
  163. }
  164. }
  165. /**
  166. *
  167. * @param pointPrev
  168. * @param pointNext
  169. * @param particle
  170. * @param min
  171. * @param max
  172. * @private
  173. */
  174. _drawStroke(pointPrev, pointNext, particle, min, max) {
  175. if (pointPrev && pointNext) {
  176. this.ctx.beginPath()
  177. this.ctx.moveTo(pointPrev[0], pointPrev[1])
  178. this.ctx.lineTo(pointNext[0], pointNext[1])
  179. if (typeof this.options.colorScale === 'function') {
  180. this.ctx.strokeStyle = this.options.colorScale(particle.m)
  181. } else if (Array.isArray(this.options.colorScale)) {
  182. let colorIdx = this._indexFor(
  183. particle.m,
  184. min,
  185. max,
  186. this.options.colorScale
  187. )
  188. this.ctx.strokeStyle = this.options.colorScale[colorIdx]
  189. }
  190. if (typeof this.options.lineWidth === 'function') {
  191. this.ctx.lineWidth = this.options.lineWidth(particle.m)
  192. }
  193. particle.x = particle.xt
  194. particle.y = particle.yt
  195. this.ctx.stroke()
  196. }
  197. }
  198. /**
  199. *
  200. * @returns {[]|*[]}
  201. * @private
  202. */
  203. _prepareParticlePaths() {
  204. let width = this.ctx.canvas.width
  205. let height = this.ctx.canvas.height
  206. let particleCount =
  207. typeof this.options.paths === 'function'
  208. ? this.options.paths(this)
  209. : this.options.paths
  210. let particles = []
  211. if (!this.field) {
  212. return []
  213. }
  214. for (let i = 0; i < particleCount; i++) {
  215. particles.push(
  216. this.field.randomize(
  217. {
  218. age: Math.floor(Math.random() * this.options.maxAge)
  219. },
  220. width,
  221. height,
  222. this.unProject
  223. )
  224. )
  225. }
  226. return particles
  227. }
  228. /**
  229. *
  230. */
  231. project() {
  232. throw new Error('project must be overriden')
  233. }
  234. /**
  235. *
  236. */
  237. unProject() {
  238. throw new Error('unProject must be overriden')
  239. }
  240. /**
  241. *
  242. * @param coordinates
  243. */
  244. intersectsCoordinate(coordinates) {
  245. throw new Error('must be override')
  246. }
  247. /**
  248. *
  249. */
  250. prerender() {
  251. if (!this.field) {
  252. return
  253. }
  254. this.particles = this._prepareParticlePaths()
  255. if (!this.starting && !this.forceStop) {
  256. this.starting = true
  257. this._then = Date.now()
  258. this.animate()
  259. }
  260. }
  261. /**
  262. *
  263. * @returns {WindCanvas}
  264. */
  265. render() {
  266. this._moveParticles()
  267. this._drawParticles()
  268. return this
  269. }
  270. clearCanvas() {
  271. this.stop()
  272. this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
  273. this.forceStop = false
  274. }
  275. /**
  276. *
  277. */
  278. start() {
  279. this.starting = true
  280. this.forceStop = false
  281. this._then = Date.now()
  282. this.animate()
  283. }
  284. /**
  285. *
  286. */
  287. stop() {
  288. cancelAnimationFrame(this.animationLoop)
  289. this.starting = false
  290. this.forceStop = true
  291. }
  292. /**
  293. *
  294. */
  295. animate() {
  296. if (this.animationLoop) {
  297. cancelAnimationFrame(this.animationLoop)
  298. }
  299. this.animationLoop = requestAnimationFrame(this.animate)
  300. let now = Date.now()
  301. let delta = now - this._then
  302. if (delta > this.options.frameRate) {
  303. this._then = now - (delta % this.options.frameRate)
  304. this.render()
  305. }
  306. }
  307. /**
  308. *
  309. * @param field
  310. * @returns {WindCanvas}
  311. */
  312. setData(field) {
  313. this.field = field
  314. return this
  315. }
  316. /**
  317. *
  318. * @param options
  319. * @returns {WindCanvas}
  320. */
  321. setOptions(options) {
  322. this.options = options
  323. if (!this.options?.maxAge && this.options?.particleAge) {
  324. this.options.maxAge = Number(this.options.particleAge)
  325. }
  326. if (!this.options?.paths && this.options?.particleMultiplier) {
  327. this.options.paths = Math.round(
  328. this.options.width *
  329. this.options.height *
  330. Number(this.options.particleMultiplier)
  331. )
  332. }
  333. return this
  334. }
  335. }
  336. export default WindCanvas