block by Rich-Harris 159d69d889b7e8ff6ffe0425807c9d47

measiles

Full Screen

index.html

<!doctype html>
<head>
	<link rel='stylesheet' href='styles.css'>
</head>
<body>
	<svg></svg>

	<label>
		<input type='checkbox'> show paths
	</label>

	<script src='d3.v3.min.js'></script>
	<script src='app.js'></script>
</body>

app.js

var svg = document.querySelector( 'svg' );
var checkbox = document.querySelector( 'input' );

var xSpeed = 0.5; // pixels per millisecond

svg.addEventListener( 'click', event => {
	const target = {
		node: document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' ),
		x: event.clientX,
		y: event.clientY
	};

	target.node.setAttribute( 'cx', target.x );
	target.node.setAttribute( 'cy', target.y );
	target.node.setAttribute( 'class', 'target' );
	target.node.setAttribute( 'r', 10 );

	svg.appendChild( target.node );

	const start = {
		x: Math.random() < 0.5 ? -50 : window.innerWidth + 50,
		y: Math.random() * window.innerHeight / 2
	};

	const dx = target.x - start.x;
	const dy = target.y - start.y;

	const distance = Math.sqrt( dx * dx + dy * dy );  // pythagoras
	const duration = distance / xSpeed;

	const peakOffset = -( Math.abs( dx ) / 4 ); // pixels
	const initialVelocity = 4 * peakOffset; // pixels per second
	const gravity = -initialVelocity;

	const attackStart = Date.now();
	const bounceStart = attackStart + duration;

	const circle = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
	circle.setAttribute( 'r', '5' );
	circle.setAttribute( 'fill', '#b51800' );
	svg.appendChild( circle );

	function attackPositionAt ( t ) {
		const yOffset = ( initialVelocity * t ) + ( gravity * t * t );

		const x = start.x + ( t * dx );
		const y = start.y + ( t * dy ) + yOffset;

		return { x, y };
	}

	function bouncePositionAt ( t ) {
		return {
			x: target.x + ( t * vx ),
			y: target.y + ( t * vy ) + ( gravity * t * t ),
			opacity: Math.pow( 1 - t, 2 )
		};
	}

	function attack () {
		const timeNow = Date.now();
		const elapsed = timeNow - attackStart;

		const t = elapsed / duration;

		if ( t >= 1 ) {
			// we've hit the target
			vx = 0.7 * -dx;
			vy = 0.7 * ( -4 * peakOffset );
			target.node.style.fill = '#b51800';

			bounce();
		} else {
			requestAnimationFrame( attack );

			const pos = attackPositionAt( t );
			circle.setAttribute( 'cx', pos.x );
			circle.setAttribute( 'cy', pos.y );
		}
	}

	function bounce () {
		const timeNow = Date.now();
		const elapsed = timeNow - bounceStart;

		const pos = bouncePositionAt( elapsed / duration );
		circle.setAttribute( 'cx', pos.x );
		circle.setAttribute( 'cy', pos.y );
		circle.style.opacity = pos.opacity;

		if ( pos.y > window.innerHeight + 50 ) {
			cleanup();


			return;
		}

		requestAnimationFrame( bounce );
	}

	function cleanup () {
		target.node.style.opacity = 0;
		setTimeout( () => svg.removeChild( target.node ), 1000 );

		if ( projection ) {
			projection.style.opacity = 0;
			setTimeout( () => svg.removeChild( projection ), 1000 );
		}
	}

	if ( checkbox.checked ) {
		var projection = document.createElementNS( 'http://www.w3.org/2000/svg', 'g' );
		for ( let t = 0; t < 1; t += 0.01 ) {
			const dot = document.createElementNS( 'http://www.w3.org/2000/svg', 'circle' );
			const pos = attackPositionAt( t );

			dot.setAttribute( 'cx', pos.x );
			dot.setAttribute( 'cy', pos.y );
			dot.setAttribute( 'r', 2 );

			projection.appendChild( dot );
		}

		svg.appendChild( projection );
	}

	attack();
});

styles.css

svg {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
}

.target {
	stroke: black;
	fill: transparent;
	transition: opacity 0.8s;
}

g {
	transition: opacity 0.8s;
}

label {
	position: relative;
}