Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

WindCanvas.js 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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. if (
  125. pointNext &&
  126. pointPrev &&
  127. pointNext[0] &&
  128. pointNext[1] &&
  129. pointPrev[0] &&
  130. pointPrev[1] &&
  131. particle.age <= this.options.maxAge
  132. ) {
  133. this._drawStroke(pointPrev, pointNext, particle, min, max)
  134. }
  135. }
  136. /**
  137. *
  138. * @param particle
  139. * @param min
  140. * @param max
  141. */
  142. _drawCoordsParticle(particle, min, max) {
  143. let source = [particle.x, particle.y]
  144. let target = [particle.xt, particle.yt]
  145. if (
  146. target &&
  147. source &&
  148. target[0] &&
  149. target[1] &&
  150. source[0] &&
  151. source[1] &&
  152. this.intersectsCoordinate(target) &&
  153. particle.age <= this.options.maxAge
  154. ) {
  155. let pointPrev = this.project(source)
  156. let pointNext = this.project(target)
  157. this._drawStroke(pointPrev, pointNext, particle, min, max)
  158. }
  159. }
  160. /**
  161. *
  162. * @param pointPrev
  163. * @param pointNext
  164. * @param particle
  165. * @param min
  166. * @param max
  167. * @private
  168. */
  169. _drawStroke(pointPrev, pointNext, particle, min, max) {
  170. if (pointPrev && pointNext) {
  171. this.ctx.beginPath()
  172. this.ctx.moveTo(pointPrev[0], pointPrev[1])
  173. this.ctx.lineTo(pointNext[0], pointNext[1])
  174. if (typeof this.options.colorScale === 'function') {
  175. this.ctx.strokeStyle = this.options.colorScale(particle.m)
  176. } else if (Array.isArray(this.options.colorScale)) {
  177. let colorIdx = this._indexFor(
  178. particle.m,
  179. min,
  180. max,
  181. this.options.colorScale
  182. )
  183. this.ctx.strokeStyle = this.options.colorScale[colorIdx]
  184. }
  185. if (typeof this.options.lineWidth === 'function') {
  186. this.ctx.lineWidth = this.options.lineWidth(particle.m)
  187. }
  188. particle.x = particle.xt
  189. particle.y = particle.yt
  190. this.ctx.stroke()
  191. }
  192. }
  193. /**
  194. *
  195. * @returns {[]|*[]}
  196. * @private
  197. */
  198. _prepareParticlePaths() {
  199. let width = this.ctx.canvas.width
  200. let height = this.ctx.canvas.height
  201. let particleCount =
  202. typeof this.options.paths === 'function'
  203. ? this.options.paths(this)
  204. : this.options.paths
  205. let particles = []
  206. if (!this.field) {
  207. return []
  208. }
  209. for (let i = 0; i < particleCount; i++) {
  210. particles.push(
  211. this.field.randomize(
  212. {
  213. age: Math.floor(Math.random() * this.options.maxAge)
  214. },
  215. width,
  216. height,
  217. this.unProject
  218. )
  219. )
  220. }
  221. return particles
  222. }
  223. /**
  224. *
  225. */
  226. project() {
  227. throw new Error('project must be overriden')
  228. }
  229. /**
  230. *
  231. */
  232. unProject() {
  233. throw new Error('unProject must be overriden')
  234. }
  235. /**
  236. *
  237. * @param coordinates
  238. */
  239. intersectsCoordinate(coordinates) {
  240. throw new Error('must be override')
  241. }
  242. /**
  243. *
  244. */
  245. prerender() {
  246. if (!this.field) {
  247. return
  248. }
  249. this.particles = this._prepareParticlePaths()
  250. if (!this.starting && !this.forceStop) {
  251. this.starting = true
  252. this._then = Date.now()
  253. this.animate()
  254. }
  255. }
  256. /**
  257. *
  258. * @returns {WindCanvas}
  259. */
  260. render() {
  261. this._moveParticles()
  262. this._drawParticles()
  263. return this
  264. }
  265. clearCanvas() {
  266. this.stop()
  267. this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height)
  268. this.forceStop = false
  269. }
  270. /**
  271. *
  272. */
  273. start() {
  274. this.starting = true
  275. this.forceStop = false
  276. this._then = Date.now()
  277. this.animate()
  278. }
  279. /**
  280. *
  281. */
  282. stop() {
  283. cancelAnimationFrame(this.animationLoop)
  284. this.starting = false
  285. this.forceStop = true
  286. }
  287. /**
  288. *
  289. */
  290. animate() {
  291. if (this.animationLoop) {
  292. cancelAnimationFrame(this.animationLoop)
  293. }
  294. this.animationLoop = requestAnimationFrame(this.animate)
  295. let now = Date.now()
  296. let delta = now - this._then
  297. if (delta > this.options.frameRate) {
  298. this._then = now - (delta % this.options.frameRate)
  299. this.render()
  300. }
  301. }
  302. /**
  303. *
  304. * @param field
  305. * @returns {WindCanvas}
  306. */
  307. setData(field) {
  308. this.field = field
  309. return this
  310. }
  311. /**
  312. *
  313. * @param options
  314. * @returns {WindCanvas}
  315. */
  316. setOptions(options) {
  317. this.options = options
  318. if (!this.options?.maxAge && this.options?.particleAge) {
  319. this.options.maxAge = Number(this.options.particleAge)
  320. }
  321. if (!this.options?.paths && this.options?.particleMultiplier) {
  322. this.options.paths = Math.round(
  323. this.options.width *
  324. this.options.height *
  325. Number(this.options.particleMultiplier)
  326. )
  327. }
  328. return this
  329. }
  330. }
  331. export default WindCanvas