📜 ⬆️ ⬇️

JavaScript hyperspace

Habrovchane! Happy cosmonautics day!



In one project, timed to coincide with today's holiday, designers were given the task of creating an imitation of hyperspace. After some thought, I decided that it would be more correct to use the Canvas element - there are a lot of elements for SVG, and the browser support is not so good, the video is too large, which means that the file size is too large and the download is long. Canvas, by the way, is also not an ideal option - it heavily loads the processor and takes a relatively large amount of RAM. But still…
')
Field entry
The code will use the reqestAnimFrame function normalizing the work of requestAnimationFrame in different browsers, and the same simple function of randomly generating an integer number. Here is their source code:
window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })(); function getRandomInt(min, max){ return Math.floor( Math.random() * (max - min + 1) ) + min; } 


Approach 1. Overlay scaled image.

The design was a picture of the starry sky, which I decided to use. Took, uploaded, superimposed on Canvas. Then he added with an increase of one more pixel. Then another. The picture is beautiful, but somehow it will not work out.

Full code and demo on jsfiddle .

There is nothing to comment on in the code, so just the code.

This approach has no life for several reasons:

Approach 2. Combat.

It was decided to draw each star and train from it separately.

Full code and demo on jsfiddle . Motion hung on mousemove event.

So, create an array of stars, generate initial values ​​for them. Here, x and y, of course, the coordinates of the star, currentLife - an indicator of the current length of the plume from the star, nx, ny and life are used to reinitialize the star, after stopping. color is one of the options in the colors array. In principle, any color could be made at all, but a feature in limiting the number of available colors will come in handy later. There are two arrays, so at the moment of attenuation it is necessary to show moving and fixed stars simultaneously. Of course, this can (and, probably, even need to) be done through a single array with a separate property of a star, but further logic depends on it and therefore I am too lazy to rewrite everything.

  var colors = ["white","rgb(200,200,255)"]; function newStar(){ var life = getRandomInt(50,150); var dx = getRandomInt(0,canvas.width); var dy = getRandomInt(0,canvas.height); return { x : dx, y : dy, nx : dx, ny : dy, life : life, currentLife : life, color : colors[getRandomInt(0,1)] }; } var stars = []; var finStars = []; var maxStars = 350; for(var i = 0; i < maxStars; i++){ finStars.push(newStar()); } 


Now let's talk about the star display. Here we include simple math:



I think it is not necessary to explain that dx refers to dy in the same way as ax to ay. If we take dx equal to the value of currentLife, then dy = currentLife * (y - cy) / (x - cx). In addition, each star has two states - when the plume grows and when it decreases. Implement it simply enough in 4 values: 2 constant and 2 variables. We draw from (var1> const1? Var1: const1) to (var2 <const2? Var2: const2). We get first a growing, and then a fading star.



It remains to calculate all this:

  var x = stars[j].x, // (x,y) -  const1 y = stars[j].y, dx = cx - stars[j].x, dy = cy - stars[j].y; if ( Math.abs(dx) > Math.abs(dy) ){ var xLife = dx > 0 ? stars[j].life : - stars[j].life, // (xLife, yLife) - const2.  star.life    ""  yLife = xLife * dy / dx, xCur = dx > 0 ? - stars[j].currentLife : stars[j].currentLife, // (xCur,yCur) -var1 yCur = xCur * dy / dx, xLast = dx > 0 ? xCur + stars[j].life : xCur - stars[j].life, // (xLast,yLast) - var2 yLast = xLast * dy / dx, mod = "x"; } else { var yLife = dy > 0 ? stars[j].life : - stars[j].life, xLife = yLife * dx / dy, yCur = dy > 0 ? - stars[j].currentLife : stars[j].currentLife, xCur = yCur * dx / dy, yLast = dy > 0 ? yCur + stars[j].life : yCur - stars[j].life, xLast = yLast * dx / dy, mod = "y"; } if(dx > 0 && dy > 0) { var qx = x - ( xLife < xLast ? xLife : xLast); var qy = y - ( yLife < yLast ? yLife : yLast); ctx.moveTo( qx < cx ? qx : cx, qy < cy ? qy : cy); var sx = x - ( xCur > 0 ? xCur : 0); var sy = y - ( yCur > 0 ? yCur : 0); ctx.lineTo( sx < cx ? sx : cx, sy < cy ? sy : cy); if ( mod == "x"){ ctx.lineTo( qx < cx ? qx : cx, (qy < cy ? qy : cy) + 2); } else { ctx.lineTo( (qx < cx ? qx : cx) + 2, qy < cy ? qy : cy); } ctx.lineTo( qx < cx ? qx : cx, qy < cy ? qy : cy); ctx.closePath(); stars[j].nx = sx < cx ? sx : cx; stars[j].ny = sy < cy ? sy : cy; } if(dx < 0 && dy < 0) { var qx = x - ( xLife > xLast ? xLife : xLast); var qy = y - ( yLife > yLast ? yLife : yLast); ctx.moveTo( qx > cx ? qx : cx, qy > cy ? qy : cy); var sx = x - ( xCur < 0 ? xCur : 0); var sy = y - ( yCur < 0 ? yCur : 0); ctx.lineTo( sx > cx ? sx : cx, sy > cy ? sy : cy); if ( mod == "x" ){ ctx.lineTo( qx > cx ? qx : cx, (qy > cy ? qy : cy) + 2); } else { ctx.lineTo( (qx > cx ? qx : cx) + 2, qy > cy ? qy : cy); } ctx.lineTo( qx > cx ? qx : cx, qy > cy ? qy : cy); ctx.closePath(); stars[j].nx = sx > cx ? sx : cx; stars[j].ny = sy > cy ? sy : cy; } if(dx < 0 && dy > 0) { var qx = x - ( xLife > xLast ? xLife : xLast); var qy = y - ( yLife < yLast ? yLife : yLast); ctx.moveTo( qx > cx ? qx : cx, qy < cy ? qy : cy); var sx = x - ( xCur < 0 ? xCur : 0); var sy = y - ( yCur > 0 ? yCur : 0); ctx.lineTo( sx > cx ? sx : cx, sy < cy ? sy : cy); if ( mod == "x" ){ ctx.lineTo( qx > cx ? qx : cx, (qy < cy ? qy : cy) + 2); } else { ctx.lineTo( (qx > cx ? qx : cx) + 2, qy < cy ? qy : cy); } ctx.lineTo( qx > cx ? qx : cx, qy < cy ? qy : cy); ctx.closePath(); stars[j].nx = sx > cx ? sx : cx; stars[j].ny = sy < cy ? sy : cy; } if(dx > 0 && dy < 0) { var qx = x - ( xLife < xLast ? xLife : xLast); var qy = y - ( yLife > yLast ? yLife : yLast); ctx.moveTo( qx < cx ? qx : cx, qy > cy ? qy : cy); var sx = x - ( xCur > 0 ? xCur : 0); var sy = y - ( yCur < 0 ? yCur : 0); ctx.lineTo( sx < cx ? sx : cx, sy > cy ? sy : cy); if ( mod == "x" ){ ctx.lineTo( qx < cx ? qx : cx, (qy > cy ? qy : cy) + 2); } else { ctx.lineTo( (qx < cx ? qx : cx) + 2, qy > cy ? qy : cy); } ctx.lineTo( qx < cx ? qx : cx, qy > cy ? qy : cy); ctx.closePath(); stars[j].nx = sx < cx ? sx : cx; stars[j].ny = sy > cy ? sy : cy; } 


Depending on which quarter our star falls in, the signs in front of the values ​​and comparisons differ, so the code is almost duplicated 4 times. In addition, the variable mod is stored in which coordinate to be considered the leading one (that is, equate dx with currentLife or dy). Without mod-a, stars near the y-axis will “fly” too quickly, because of a large angle.

And the last remark - in the original only two colors are used, therefore in one pass the drawing on the Canvas occurs only two times (since two colors are indicated). All stars of the same color are formed in one way, after which they are displayed on the Canvas.

It remains to wrap all this in a loop and run.

Approach 3. Right.

By the right approach, I mean the use of common ready-made solutions and libraries. Finished solutions for a cursory inspection was not found. As a library, I decided to try libcanvas. The blessing on Habrahabr it is presented rather strongly.

Full code and demo on jsfiddle . (JSFiddle may not load the atom and libcanvas from github, so it may be necessary to reload the page several times)

The result was the following:

  new function () { var center, i, helper, stars; LibCanvas.extract(); helper = new App.Light(new Size( document.width, document.height)); center = helper.app.rectangle.center; stars = []; for(i = 0; i < 350; i++){ new function() { var point = new Point(getRandomInt(document.width/2,document.width),document.height/2), length = getRandomInt(50,150), angle = getRandomInt(0,360), coords = [ new Point(0,0), new Point(0,0), new Point(0,0) ], path = helper.createVector( new Path() .moveTo( coords[0] ) .lineTo( coords[1] ) .lineTo( coords[2] ) .lineTo( coords[0] )).setStyle({fill:"rgb(150,150,150)",stroke:"rgb(150,150,150)"}); point.rotate( - angle.degree(), center); var star = { point : point, length : length, angle : angle, coords : coords, live : 0, setLength : function(){ if (arguments.length > 0){ this.live = arguments[0]; } this.coords[0].x = this.point.x; this.coords[0].y = this.point.y; this.coords[1].x = this.coords[0].x + this.live * Math.cos( this.angle.degree() ); this.coords[1].y = this.coords[0].y - this.live * Math.sin( this.angle.degree() ); this.coords[2].x = this.coords[1].x + 2 * Math.sin( this.angle.degree() ); this.coords[2].y = this.coords[1].y + 2 * Math.cos( this.angle.degree() ); }, path : path }; star.setLength(); stars.push(star); }; } setInterval(function(){ for(var i = 0; i < 350; i++){ stars[i].setLength( stars[i].live + 1 ); stars[i].path.redraw(); } },10); }; 


I must say that here, in contrast to the combat version, adequate mathematics with trigonometry is used, but I did not begin to write on the same functionality. The speed of code execution on libCanvas is not much different from the native method, but the code is several times smaller and the development speed is much higher. From the very beginning, I did not use libCanvas for several reasons: I have never used it before, I got used to pure JavaScript and I was afraid that the add-in version would be noticeably slower. As it turned out he was afraid in vain.


UPD Correct implementation on TheShock LibCanvas

An example on jsfiddle . Looks especially nice in fullScreen .

The creation and adjustment of a star is taken out in a separate class. For each star is determined by the speed, color and lifetime.

 atom.declare( 'Star', App.Element, { progress: 0, opacity: 1, configure: function () { var screenRectangle = this.layer.ctx.rectangle; this.animate = new atom.Animatable(this).animate; this.from = screenRectangle.getRandomPoint(); this.shape = new Polygon(this.from.clone(), this.from.clone(), this.from.clone()); this.angle = -this.from.angleTo(screenRectangle.center); this.sin = this.angle.sin(); this.cos = this.angle.cos(); this.progressSpeed = Math.random() + 0.5; this.color = new atom.Color(128, 128, Number.random(128, 192)).toString(); setTimeout(this.fadeOut.bind(this), Number.random(1000, 8000)); }, fadeOut: function () { this.animate({ time: 2000, props: { opacity: 0 }, onComplete: this.destroy }); }, onUpdate: function () { var sin = this.sin, cos = this.cos, p = this.shape.points; this.progress += this.progressSpeed; p[1].x = p[0].x + this.progress * cos; p[1].y = p[0].y - this.progress * sin; p[2].x = p[1].x + 2 * sin; p[2].y = p[1].y + 2 * cos; this.redraw(); }, renderTo: function (ctx) { ctx.save(); if (this.opacity < 1) ctx.set({ globalAlpha: this.opacity }); ctx.fill( this.shape, this.color ); ctx.restore(); } }); 


Initially, fewer stars are created, and then added to the frame in the caj:

 new function () { var helper = new App.Light( new Size(document.width || 800, document.height || 800), { intersection: 'full', invoke: true } ); for(i = 0; i < 200; i++) new Star(helper.layer); atom.frame.add(function () { new Star(helper.layer); }); }; 

// UPD end

That's all and again with the day of astronautics!

References:
An example with a picture on jsfiddle .
“Combat” example on jsfiddle .
AtomJS and libCanvas for the third example.
The third example is libCanvas on jsfiddle . (it may not work right away due to the peculiarities of jsfiddle and github)
Promo site for which the effect was created.

Source: https://habr.com/ru/post/176325/


All Articles