Created
April 4, 2019 13:53
-
-
Save ssfang/48c53be926f0ed6bd597062897ee3ec2 to your computer and use it in GitHub Desktop.
Colorful Pie Puzzle wheels // source https://jsbin.com/lazidox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="description" content="wheels"> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>Colorful Pie Puzzle</title> | |
<style id="jsbin-css"> | |
body { | |
margin: 0px; | |
padding: 0px; | |
width: 100%; | |
} | |
/* https://stackoverflow.com/questions/10614481/disable-double-tap-zoom-option-in-browser-on-touch-devices */ | |
.disable-dbl-tap-zoom, .mycanvas { | |
touch-action: manipulation; | |
} | |
</style> | |
</head> | |
<body> | |
<strong>彩盘转转转 Colorful Pies Puzzle</strong> | |
<p>目标:转动外围彩盘使得颜色和中间彩盘的一致。</p> | |
<p>Goal: turn surrounding pies to match with the center pie.</p> | |
<canvas id="mycanvas" class="mycanvas" width="360" height="400"></canvas> | |
<script> | |
var mySearchParams = new URLSearchParams(window.location.search); | |
var spName = mySearchParams.get('name'); | |
if(spName){ | |
var helloElement = document.createElement('p'); | |
helloElement.textContent = 'Hello ' + spName + '!'; | |
document.body.appendChild(helloElement); | |
} | |
console.log(spName); | |
</script> | |
<script id="jsbin-javascript"> | |
try{ top.document.getElementById('codefund_ad').remove(); } catch(e){ console.log(e); } | |
/** | |
* @param {string} html Text that contains text and tags to be converted to a document fragment. e.g. '<div>x</div><span>y</span><br/>' | |
* @return {DocumentFragment} a minimal document object that has no parent (a lightweight version of `Document`) | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/Range/selectNodeContents | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/range/createContextualFragment | |
* @example document.body.appendChild($j('<div>x</div><span>y</span><br/>')); | |
* @since IE11 | |
*/ | |
function $j(html) { | |
/// Way 1 | |
//var temp = document.createElement('template'); | |
//temp.innerHTML = html; | |
//return temp.content; | |
/// Way 2 | |
// var range = document.createRange(); | |
// Set body as context node or whatever context the fragment is to be evaluated in. | |
// range.selectNodeContents(document.body); // range.selectNode(document.body); | |
// return range.createContextualFragment(html); | |
return document.createRange().createContextualFragment(html); | |
} | |
/** | |
* @typedef Wheel | |
* @type {string[] | {x: number, y: number, radius: number, rotate: number, pointer: number, direction: number, originAngle: number}} | |
* @property {number} x | |
* @property {number} y | |
* @property {number} radius | |
* @property {number} rotate | |
* @property {number} direction | |
* @property {number} originAngle the starting angle to draw this wheel | |
* @property {number|null} pointer The array index of the segment sector pointed to the center | |
* @property {string} name | |
*/ | |
/** @type {Wheel} init each array clockwise */ | |
var centerWheel = ['red', 'green', 'gray', 'gold', 'pink', 'purple']; // Idler wheel / driven wheel | |
/** | |
* @type {Wheel[]} Each of the top points of the 4 smaller stars are rotated such that they point towards the center point of the larger star. | |
* init each array counterclockwise and these wheels rotate around their center clockwise | |
* @see https://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons | |
*/ | |
var wheels = [ //Drive wheels | |
['red', 'gold', 'red', 'green', 'pink', 'purple'], | |
['gray', 'gold', 'gray', 'red', 'pink', 'purple'], | |
['gray', 'gray', 'green', 'gray', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'purple', 'purple'] | |
]; | |
//centerWheel.length = 4; | |
//wheels.length = 4; | |
//wheels.forEach(function (it){ it.length=4; }); | |
/** | |
* Draw an isosceles triangle as an arrowhead | |
``` | |
|\ | |
+--------------------------+ \ | |
| > end (locx, locy) | |
+--------------------------+ / | |
|/ | |
|<->| sizey | |
``` | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {number} locx The x axis of the coordinate for the end of the arrow | |
* @param {number} locy The y axis of the coordinate for the end of the arrow | |
* @param {number} angle | |
* @param {number} sizex The remaining/base/hem side's length, not the two equal sides (legs). | |
* @param {number} sizey The Height, i.e. the distance from top to bottom. Or at right angles | |
* from any base to the furthest corner. | |
* @see https://stackoverflow.com/questions/6576827/html-canvas-draw-curved-arrows#6577443 | |
* @see https://github.com/frogcat/canvas-arrow | |
* @see https://en.wikipedia.org/wiki/Arrow#Arrowhead | |
* @see https://en.wikipedia.org/wiki/Isosceles_triangle | |
*/ | |
function drawArrowhead(ctx, locx, locy, angle, sizex, sizey) { | |
var hx = sizex / 2; | |
var hy = sizey / 2; | |
ctx.save(); | |
ctx.translate(locx, locy); | |
ctx.rotate(angle); | |
ctx.translate(-hx, -hy); | |
ctx.beginPath(); | |
ctx.moveTo(0, 0); | |
ctx.lineTo(0, sizey); | |
ctx.lineTo(sizex, hy); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.restore(); | |
} | |
/** | |
* | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {string[]&{x:number, y:number, radius:number}} wheel | |
* @param {string | CanvasGradient | CanvasPattern} strokeStyle | |
* @param {number} directionAngle The angle of the line of the center wheel and this wheel's centers, | |
* measured clockwise around the center wheel from the positive x-axis and expressed in radians. | |
*/ | |
function drawPie(ctx, wheel, strokeStyle) { | |
if(wheel.action){ | |
wheel.action.act(ctx.deltaTime, wheel); | |
} | |
const lineWidth = 1; | |
const innerRadius = 15; | |
const innerStartRadius = innerRadius - lineWidth / 2; | |
ctx.lineWidth = lineWidth; | |
var slices = wheel.length; | |
var sliceAngle = Math.PI / slices; | |
//https://en.wikipedia.org/wiki/Angular_distance // | |
var spaceAngle = sliceAngle; //the angle between the end side of the slice and the start side of the next adjacent slice. | |
for (var idxSlice = 0, angle = wheel.directionAngle + Math.PI - sliceAngle / 2 + wheel.rotate; idxSlice < slices; ++idxSlice) { | |
// Begin a path as the segment consists of an arc and 2 lines. | |
ctx.beginPath(); | |
if (null == wheel.direction) { | |
ctx.moveTo(wheel.x, wheel.y); // ctx.moveTo(centerX, centerY); | |
} else { | |
// Work out the x and y values for the starting point of the segment which is at its starting angle | |
// but out from the center point of the wheel by the value of the innerRadius. Some correction for line width is needed. | |
// Now move here relative to the center point of the wheel. | |
ctx.moveTo(wheel.x + Math.cos(angle) * innerStartRadius, wheel.y + Math.sin(angle) * innerStartRadius); | |
} | |
// Draw the outer arc of the segment clockwise in direction --> | |
// `startAngle`: The angle at which the arc starts, measured clockwise from the positive x-axis and expressed in radians. | |
ctx.arc(wheel.x, wheel.y, wheel.radius, angle, angle + sliceAngle); | |
if (null != wheel.direction) { | |
// Draw another arc, this time anticlockwise <-- at the innerRadius between the end angle and the start angle. | |
// Canvas will draw a connecting line from the end of the outer arc to the beginning of the inner arc completing the shape. | |
ctx.arc(wheel.x, wheel.y, innerRadius, angle + sliceAngle, angle, true); | |
} else { | |
// If no inner radius then we draw a line back to the center of the wheel. | |
// Draw a line back to the center of the wheel. `closePath` automatically connects the shape's first and last points. | |
//ctx.closePath(); ctx.lineTo(centerX, centerY); | |
} | |
ctx.closePath(); | |
ctx.fillStyle = wheel[idxSlice]; | |
// Fill and stroke the segment. | |
if (idxSlice === wheel.pointer) { | |
var shadowColor = ctx.shadowColor; | |
//https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur | |
//https://jsbin.com/gavepub/edit?html,js,output | |
ctx.shadowBlur = 25; | |
ctx.shadowColor = 'blue'; | |
//Shadows are only drawn if the shadowColor property is set to a non-transparent value. One of the | |
//shadowBlur, shadowOffsetX, or shadowOffsetY properties must be non-zero, as well. | |
//ctx.shadowOffsetX = 0; // The default value is 0 (no vertical offset). | |
//ctx.shadowOffsetY = 0; // The default value is 0 (no vertical offset). | |
ctx.fill(); | |
ctx.strokeStyle = 'blue'; | |
ctx.stroke(); | |
ctx.shadowBlur = 0; //The default value is 0. | |
ctx.shadowColor = shadowColor; //The default value is fully-transparent black 'rgba(0, 0, 0, 0)' | |
} else { | |
ctx.fill(); | |
ctx.strokeStyle = strokeStyle; | |
ctx.stroke(); | |
} | |
// only for debug test | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "white"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
var halfwayAngle = angle + sliceAngle / 2; // symmetry axis | |
var textRadius = 0.8 * wheel.radius; | |
ctx.fillText(idxSlice, wheel.x + textRadius * Math.cos(halfwayAngle), wheel.y + textRadius * Math.sin(halfwayAngle)); | |
angle -= sliceAngle + spaceAngle; | |
} | |
// draw Circle border | |
ctx.beginPath(); | |
ctx.arc(wheel.x, wheel.y, wheel.radius, 0, Math.PI * 2); | |
ctx.strokeStyle = strokeStyle; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
if (null != wheel.direction) { | |
// draw the rotation direction indicator | |
ctx.beginPath(); | |
ctx.fillStyle = 'rgba(55, 217, 56,1)'; | |
ctx.strokeStyle = 'rgba(55, 217, 56,1)'; | |
var endAngle = wheel.directionAngle + Math.PI / 10; | |
ctx.arc(wheel.x, wheel.y, wheel.radius + 8, wheel.directionAngle - Math.PI / 10, endAngle); | |
ctx.stroke(); | |
var lastX = wheel.x + (wheel.radius + 8) * Math.cos(endAngle); | |
var lastY = wheel.y + (wheel.radius + 8) * Math.sin(endAngle); | |
// var ang = findAngle(sx, sy, ex, ey); | |
// ctx.fillRect(lastX, lastY, 4, 4); | |
drawArrowhead(ctx, lastX, lastY, Math.PI / 2 + endAngle, 12, 12); | |
} | |
// only for debug test | |
if (null != wheel.name) { | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "cyan"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
ctx.fillText(wheel.name, wheel.x, wheel.y); | |
} | |
// if (null != wheel.pointer) { | |
// ctx.fillStyle = 'blue'; | |
// ctx.fillText(wheel.pointer, wheel.x + 38, wheel.y); | |
// } | |
} | |
function startRotateByAction(wheel, delta) { | |
var action; | |
if(wheel.action){ | |
action = wheel.action; | |
action.finish(wheel); | |
} else{ | |
action = {}; | |
wheel.action = action; | |
} | |
action.time = 0; | |
action.duration = 500; | |
action.start = wheel.rotate; | |
action.delta = delta; | |
action.finish = function (wheel){ | |
const radian360 = 2 * Math.PI; | |
const endAngle = this.start + this.delta; | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
wheel.rotate = (endAngle % radian360) + (endAngle < 0 ? radian360 : 0); | |
}; | |
action.act = function(interval, wheel){ | |
if(this.time < this.duration){ | |
this.time += interval; | |
wheel.rotate = this.start + this.time/this.duration * this.delta; | |
} else { | |
this.finish(wheel); | |
wheel.action = null; | |
} | |
}; | |
} | |
function startLoop() { | |
var steps = 0; // counts the rotation numbers, number of revolution | |
/** @type {HTMLCanvasElement} */ | |
var canvas = document.getElementById('mycanvas'); | |
if (!canvas) { | |
canvas = document.createElement('canvas'); | |
canvas.className = 'mycanvas'; | |
document.body.appendChild(canvas); | |
} | |
canvas.addEventListener('click', onDriveWheelClicked); | |
//canvas.addEventListener('mozfullscreenchange', onFullScreenChange); | |
//canvas.addEventListener('webkitfullscreenchange', onFullScreenChange); | |
//requestFullScreen(canvas); called when event is triggered | |
//https://stackoverflow.com/questions/13157586/full-screen-canvas-on-mobile-devices | |
function requestFullScreen(el) { | |
if (el.webkitRequestFullScreen) { | |
// Google Chrome Version 73.0.3683.86 (Official Build) (32-bit) | |
//Failed to execute 'requestFullscreen' on 'Element': API can only be initiated by a user gesture. | |
el.webkitRequestFullScreen(); | |
} else { | |
el.mozRequestFullScreen(); | |
} | |
} | |
var context = canvas.getContext('2d'); | |
var count = wheels.length; | |
var centerAngle = 2 * Math.PI / count; | |
var cx = canvas.width / 2, | |
cy = canvas.height / 2; | |
var radius = 120; | |
// calculate the starting angle to make it symmetric with respect to the y-axis | |
//But draw piles clockwise from the positive y axis (12 oclock). | |
angle = count % 2 ? Math.PI / -2 : Math.PI / -2 - centerAngle / 2; // is odd? | |
centerWheel.rotate = 0; | |
centerWheel.x = cx; | |
centerWheel.y = cy; | |
centerWheel.radius = radius / 2; | |
centerWheel.directionAngle = Math.PI + angle; | |
// https://www.mathsisfun.com/geometry/regular-polygons.html | |
for (var idxPie = 0; idxPie < count; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
//angle = startAngle + (idx * centerAngle); | |
var vx = cx + radius * Math.cos(angle); | |
var vy = cy + radius * Math.sin(angle); | |
wheel.direction = idxPie; | |
wheel.name = String.fromCharCode(65+idxPie); | |
wheel.x = vx; | |
wheel.y = vy; | |
wheel.radius = radius / 2; | |
wheel.rotate = 0; //Math.PI + idxPie * centerAngle. clamp the angle to the range 0 to 1 in turns | |
wheel.directionAngle = angle; | |
drawPie(context, wheel, 'black', angle); | |
} | |
drawPie(context, centerWheel, 'black'); | |
requestAnimationFrame(animate); | |
/** | |
* @param {MouseEvent} me | |
* @see https://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element | |
*/ | |
function onDriveWheelClicked(me) { | |
// Modern browser's now handle this for you. Chrome, IE9, and Firefox support the offsetX/Y | |
// like this, passing in the event from the click handler. | |
var x = me.offsetX, | |
y = me.offsetY; | |
// if (me.pageX || me.pageY) { | |
// x = me.pageX; | |
// y = me.pageY; | |
// } else { | |
// x = me.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; | |
// y = me.clientY + document.body.scrollTop + document.documentElement.scrollTop; | |
// } | |
// var canvasElement = me.currentTarget; | |
// x -= canvasElement.offsetLeft; | |
// y -= canvasElement.offsetTop; | |
const radian360 = 2 * Math.PI; | |
const pieCount = wheels.length; | |
const deltaAngle = radian360 / pieCount; | |
for (var idxPie = 0; idxPie < pieCount; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
if (Math.pow(x - wheel.x, 2) + Math.pow(y - wheel.y, 2) <= Math.pow(wheel.radius, 2)) { | |
//var rotate = wheel.rotate + deltaAngle; //clockwise | |
//wheel.rotate = rotate % radian360; // clamp the angle to the range 0 to `2 * Math.PI in radians | |
// ClampAngle Limit the angle to the range 0 to 1 in turns | |
//rotate = centerWheel.rotate - deltaAngle; // Counterclockwise | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
//centerWheel.rotate = (rotate % radian360) + (rotate < 0 ? radian360 : 0); | |
startRotateByAction(wheel, deltaAngle); | |
startRotateByAction(centerWheel, -deltaAngle); | |
++steps; | |
} | |
} | |
var event = me || window.event; | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
//https://jlongster.com/Going-Fullscreen-with-Canvas | |
function onFullScreenChange(evt) { | |
var canvas = evt.currentTarget; | |
if (document.mozFullScreen || document.webkitIsFullScreen) { | |
var rect = canvas.getBoundingClientRect(); | |
canvas.width = rect.width; | |
canvas.height = rect.height; | |
} else { | |
canvas.width = 500; | |
canvas.height = 400; | |
} | |
} | |
var lastTime; | |
function animate(timestamp) { | |
context.deltaTime = lastTime ? timestamp - lastTime : 0; | |
lastTime = timestamp; | |
//The clearRect() method sets the pixels in a rectangular area to transparent black (rgba(0,0,0,0)). | |
// context.clearRect(0, 0, canvas.width, canvas.height); | |
context.fillStyle = '#E3E8E6'; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
drawPie(context, centerWheel, 'black'); | |
//var logs = []; | |
var score = 0; | |
var count = centerWheel.length; | |
var centralAngle = 2 * Math.PI / count; | |
var angle = count % 2 ? Math.floor(count / 2) * centralAngle - Math.PI / 2 : centralAngle / 2; // is odd? | |
for (var idx = 0; idx < count; ++idx) { | |
// `center.rotate` is a normalized angle clamped in [0, 2 * Math.PI), so `index` is an integer in the range [0, count). | |
var index = Math.floor((centerWheel.rotate) / centralAngle); // direction0's (the first direction) index in center colors; | |
index -= idx; // the idx-th direction's index in center colors; | |
if (index < 0) { | |
index += count; | |
} | |
var wheel = wheels[idx]; | |
// Retrieves the index of the slice pointed to the center wheel | |
var pointer = Math.floor(wheel.rotate / centralAngle); //The array index of the segment sector pointed to the center | |
var matched = centerWheel[index] == wheel[pointer]; | |
wheel.pointer = matched ? pointer : null; | |
if (matched) { | |
++score; | |
} | |
//logs.push(`direction_${idx}: ${index}=${centerWheel[index]} vs ${pointer}=${wheel[pointer]}`); | |
drawPie(context, wheel, 'black', angle); | |
angle += centralAngle; | |
} | |
//console.log(logs.join(', ')); | |
//https://www.tombraiderchronicles.com/underworld/walkthrough/level02.html#pastthedoorpuzzle | |
if (count === score) { | |
context.font = context.font.replace(/\d+px/, '56px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'top'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('You win!', canvas.width / 2, 1); | |
} | |
//draw the number of steps | |
context.font = context.font.replace(/\d+px/, '28px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'bottom'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('Steps: ' + steps, canvas.width / 2, canvas.height - 1); | |
requestAnimationFrame(animate); | |
} | |
} | |
startLoop(); | |
//initWheels(wheels); | |
//console.log( wheels ); | |
//console.log(getIndex(wheels[0])); | |
/** | |
* https://developer.mozilla.org/en-US/docs/Web/CSS/conic-gradient#Customizing_Gradients | |
* @see https://github.com/leaverou/conic-gradient | |
*/ | |
//element.style.maxWidth = "100px"; | |
//transform: rotate(45deg); | |
//ELEMENT.style.setProperty('--element-width', NEW_VALUE); | |
//pieChart(wheel); | |
// Center X and center Y are the coordinates of the center point of the polygon. | |
// Set initially to 550, 550. Note that the y coordinate is positive downwards, | |
// to conform to the convention in most computer software. Positive x is to the right. | |
// Start angle (degrees): Start angle is the position of the first vertex. | |
// This angle is in degrees and is the angle starting at 3 o'clock going counter | |
// clockwise. So for example if you want the first vertex to be at 12 o'clock, set | |
// this to 90. Set initially to blank (auto). | |
// If you leave this blank it will be set automatically: If the number of sides is | |
// odd, (e.g. a pentagon), the first vertex will be at 12 o'clock. If even, e.g. an | |
// octagon, the top and bottom sides will be horizontal on the page. | |
/** | |
* This calculator takes the parameters of a regular polygon and calculates its coordinates. | |
* It produces both the coordinates of the vertices and the coordinates of the line segments | |
* making up the sides of the polygon. | |
* | |
* Note that the y coordinates are positive downwards, to conform to the convention in most | |
* computer software. Positive x is to the right. | |
* | |
* @param {number} nsides The number of sides. Must be greater than 2. Set initially to 5. | |
* @param {number} radius The distance from the center to a vertex. Set initially to 100. | |
* @param {number} startAngle | |
*/ | |
function calculatePolygonCoordinates(nsides, radius, startAngle, cx, cy) { | |
// collect inputs | |
// if(isNaN(radius) || radius<1) { alert("Radius must be a number greater than 0"); return; } | |
// if(isNaN(n) || n<3) { alert("Number of sides must be a number greater than 2"); return; } | |
// if(isNaN(startAng)) { alert("Starting angle must be a number"); return; } | |
var centerAngle = 2 * Math.PI / nsides; | |
//calculate the default start angle | |
if (!startAngle) { //none supplied | |
if (isOdd(nsides)) | |
startAngle = Math.PI / 2; //12 oclock | |
else | |
startAngle = Math.PI / 2 - centerAngle / 2; | |
} | |
var angle = startAngle, | |
vertex = []; // new Array(); | |
for (var idx = 0; idx < nsides; idx++, angle += centerAngle) { | |
//angle = startAngle + (idx * centerAngle); | |
var vx = Math.round(cx + radius * Math.cos(angle)); | |
var vy = Math.round(cy - radius * Math.sin(angle)); | |
vertex.push({ | |
x: vx, | |
y: vy | |
}); | |
} | |
function isOdd(n) { | |
return (n % 2 == 1); | |
} | |
function toRadians(degs) { | |
return Math.PI * degs / 180; | |
} | |
return vertex; | |
} | |
</script> | |
<script id="jsbin-source-css" type="text/css">body { | |
margin: 0px; | |
padding: 0px; | |
width: 100%; | |
} | |
/* https://stackoverflow.com/questions/10614481/disable-double-tap-zoom-option-in-browser-on-touch-devices */ | |
.disable-dbl-tap-zoom, .mycanvas { | |
touch-action: manipulation; | |
}</script> | |
<script id="jsbin-source-javascript" type="text/javascript">try{ top.document.getElementById('codefund_ad').remove(); } catch(e){ console.log(e); } | |
/** | |
* @param {string} html Text that contains text and tags to be converted to a document fragment. e.g. '<div>x</div><span>y</span><br/>' | |
* @return {DocumentFragment} a minimal document object that has no parent (a lightweight version of `Document`) | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/Range/selectNodeContents | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/range/createContextualFragment | |
* @example document.body.appendChild($j('<div>x</div><span>y</span><br/>')); | |
* @since IE11 | |
*/ | |
function $j(html) { | |
/// Way 1 | |
//var temp = document.createElement('template'); | |
//temp.innerHTML = html; | |
//return temp.content; | |
/// Way 2 | |
// var range = document.createRange(); | |
// Set body as context node or whatever context the fragment is to be evaluated in. | |
// range.selectNodeContents(document.body); // range.selectNode(document.body); | |
// return range.createContextualFragment(html); | |
return document.createRange().createContextualFragment(html); | |
} | |
/** | |
* @typedef Wheel | |
* @type {string[] | {x: number, y: number, radius: number, rotate: number, pointer: number, direction: number, originAngle: number}} | |
* @property {number} x | |
* @property {number} y | |
* @property {number} radius | |
* @property {number} rotate | |
* @property {number} direction | |
* @property {number} originAngle the starting angle to draw this wheel | |
* @property {number|null} pointer The array index of the segment sector pointed to the center | |
* @property {string} name | |
*/ | |
/** @type {Wheel} init each array clockwise */ | |
var centerWheel = ['red', 'green', 'gray', 'gold', 'pink', 'purple']; // Idler wheel / driven wheel | |
/** | |
* @type {Wheel[]} Each of the top points of the 4 smaller stars are rotated such that they point towards the center point of the larger star. | |
* init each array counterclockwise and these wheels rotate around their center clockwise | |
* @see https://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons | |
*/ | |
var wheels = [ //Drive wheels | |
['red', 'gold', 'red', 'green', 'pink', 'purple'], | |
['gray', 'gold', 'gray', 'red', 'pink', 'purple'], | |
['gray', 'gray', 'green', 'gray', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'purple', 'purple'] | |
]; | |
//centerWheel.length = 4; | |
//wheels.length = 4; | |
//wheels.forEach(function (it){ it.length=4; }); | |
/** | |
* Draw an isosceles triangle as an arrowhead | |
``` | |
|\ | |
+--------------------------+ \ | |
| > end (locx, locy) | |
+--------------------------+ / | |
|/ | |
|<->| sizey | |
``` | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {number} locx The x axis of the coordinate for the end of the arrow | |
* @param {number} locy The y axis of the coordinate for the end of the arrow | |
* @param {number} angle | |
* @param {number} sizex The remaining/base/hem side's length, not the two equal sides (legs). | |
* @param {number} sizey The Height, i.e. the distance from top to bottom. Or at right angles | |
* from any base to the furthest corner. | |
* @see https://stackoverflow.com/questions/6576827/html-canvas-draw-curved-arrows#6577443 | |
* @see https://github.com/frogcat/canvas-arrow | |
* @see https://en.wikipedia.org/wiki/Arrow#Arrowhead | |
* @see https://en.wikipedia.org/wiki/Isosceles_triangle | |
*/ | |
function drawArrowhead(ctx, locx, locy, angle, sizex, sizey) { | |
var hx = sizex / 2; | |
var hy = sizey / 2; | |
ctx.save(); | |
ctx.translate(locx, locy); | |
ctx.rotate(angle); | |
ctx.translate(-hx, -hy); | |
ctx.beginPath(); | |
ctx.moveTo(0, 0); | |
ctx.lineTo(0, sizey); | |
ctx.lineTo(sizex, hy); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.restore(); | |
} | |
/** | |
* | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {string[]&{x:number, y:number, radius:number}} wheel | |
* @param {string | CanvasGradient | CanvasPattern} strokeStyle | |
* @param {number} directionAngle The angle of the line of the center wheel and this wheel's centers, | |
* measured clockwise around the center wheel from the positive x-axis and expressed in radians. | |
*/ | |
function drawPie(ctx, wheel, strokeStyle) { | |
if(wheel.action){ | |
wheel.action.act(ctx.deltaTime, wheel); | |
} | |
const lineWidth = 1; | |
const innerRadius = 15; | |
const innerStartRadius = innerRadius - lineWidth / 2; | |
ctx.lineWidth = lineWidth; | |
var slices = wheel.length; | |
var sliceAngle = Math.PI / slices; | |
//https://en.wikipedia.org/wiki/Angular_distance // | |
var spaceAngle = sliceAngle; //the angle between the end side of the slice and the start side of the next adjacent slice. | |
for (var idxSlice = 0, angle = wheel.directionAngle + Math.PI - sliceAngle / 2 + wheel.rotate; idxSlice < slices; ++idxSlice) { | |
// Begin a path as the segment consists of an arc and 2 lines. | |
ctx.beginPath(); | |
if (null == wheel.direction) { | |
ctx.moveTo(wheel.x, wheel.y); // ctx.moveTo(centerX, centerY); | |
} else { | |
// Work out the x and y values for the starting point of the segment which is at its starting angle | |
// but out from the center point of the wheel by the value of the innerRadius. Some correction for line width is needed. | |
// Now move here relative to the center point of the wheel. | |
ctx.moveTo(wheel.x + Math.cos(angle) * innerStartRadius, wheel.y + Math.sin(angle) * innerStartRadius); | |
} | |
// Draw the outer arc of the segment clockwise in direction --> | |
// `startAngle`: The angle at which the arc starts, measured clockwise from the positive x-axis and expressed in radians. | |
ctx.arc(wheel.x, wheel.y, wheel.radius, angle, angle + sliceAngle); | |
if (null != wheel.direction) { | |
// Draw another arc, this time anticlockwise <-- at the innerRadius between the end angle and the start angle. | |
// Canvas will draw a connecting line from the end of the outer arc to the beginning of the inner arc completing the shape. | |
ctx.arc(wheel.x, wheel.y, innerRadius, angle + sliceAngle, angle, true); | |
} else { | |
// If no inner radius then we draw a line back to the center of the wheel. | |
// Draw a line back to the center of the wheel. `closePath` automatically connects the shape's first and last points. | |
//ctx.closePath(); ctx.lineTo(centerX, centerY); | |
} | |
ctx.closePath(); | |
ctx.fillStyle = wheel[idxSlice]; | |
// Fill and stroke the segment. | |
if (idxSlice === wheel.pointer) { | |
var shadowColor = ctx.shadowColor; | |
//https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur | |
//https://jsbin.com/gavepub/edit?html,js,output | |
ctx.shadowBlur = 25; | |
ctx.shadowColor = 'blue'; | |
//Shadows are only drawn if the shadowColor property is set to a non-transparent value. One of the | |
//shadowBlur, shadowOffsetX, or shadowOffsetY properties must be non-zero, as well. | |
//ctx.shadowOffsetX = 0; // The default value is 0 (no vertical offset). | |
//ctx.shadowOffsetY = 0; // The default value is 0 (no vertical offset). | |
ctx.fill(); | |
ctx.strokeStyle = 'blue'; | |
ctx.stroke(); | |
ctx.shadowBlur = 0; //The default value is 0. | |
ctx.shadowColor = shadowColor; //The default value is fully-transparent black 'rgba(0, 0, 0, 0)' | |
} else { | |
ctx.fill(); | |
ctx.strokeStyle = strokeStyle; | |
ctx.stroke(); | |
} | |
// only for debug test | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "white"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
var halfwayAngle = angle + sliceAngle / 2; // symmetry axis | |
var textRadius = 0.8 * wheel.radius; | |
ctx.fillText(idxSlice, wheel.x + textRadius * Math.cos(halfwayAngle), wheel.y + textRadius * Math.sin(halfwayAngle)); | |
angle -= sliceAngle + spaceAngle; | |
} | |
// draw Circle border | |
ctx.beginPath(); | |
ctx.arc(wheel.x, wheel.y, wheel.radius, 0, Math.PI * 2); | |
ctx.strokeStyle = strokeStyle; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
if (null != wheel.direction) { | |
// draw the rotation direction indicator | |
ctx.beginPath(); | |
ctx.fillStyle = 'rgba(55, 217, 56,1)'; | |
ctx.strokeStyle = 'rgba(55, 217, 56,1)'; | |
var endAngle = wheel.directionAngle + Math.PI / 10; | |
ctx.arc(wheel.x, wheel.y, wheel.radius + 8, wheel.directionAngle - Math.PI / 10, endAngle); | |
ctx.stroke(); | |
var lastX = wheel.x + (wheel.radius + 8) * Math.cos(endAngle); | |
var lastY = wheel.y + (wheel.radius + 8) * Math.sin(endAngle); | |
// var ang = findAngle(sx, sy, ex, ey); | |
// ctx.fillRect(lastX, lastY, 4, 4); | |
drawArrowhead(ctx, lastX, lastY, Math.PI / 2 + endAngle, 12, 12); | |
} | |
// only for debug test | |
if (null != wheel.name) { | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "cyan"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
ctx.fillText(wheel.name, wheel.x, wheel.y); | |
} | |
// if (null != wheel.pointer) { | |
// ctx.fillStyle = 'blue'; | |
// ctx.fillText(wheel.pointer, wheel.x + 38, wheel.y); | |
// } | |
} | |
function startRotateByAction(wheel, delta) { | |
var action; | |
if(wheel.action){ | |
action = wheel.action; | |
action.finish(wheel); | |
} else{ | |
action = {}; | |
wheel.action = action; | |
} | |
action.time = 0; | |
action.duration = 500; | |
action.start = wheel.rotate; | |
action.delta = delta; | |
action.finish = function (wheel){ | |
const radian360 = 2 * Math.PI; | |
const endAngle = this.start + this.delta; | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
wheel.rotate = (endAngle % radian360) + (endAngle < 0 ? radian360 : 0); | |
}; | |
action.act = function(interval, wheel){ | |
if(this.time < this.duration){ | |
this.time += interval; | |
wheel.rotate = this.start + this.time/this.duration * this.delta; | |
} else { | |
this.finish(wheel); | |
wheel.action = null; | |
} | |
}; | |
} | |
function startLoop() { | |
var steps = 0; // counts the rotation numbers, number of revolution | |
/** @type {HTMLCanvasElement} */ | |
var canvas = document.getElementById('mycanvas'); | |
if (!canvas) { | |
canvas = document.createElement('canvas'); | |
canvas.className = 'mycanvas'; | |
document.body.appendChild(canvas); | |
} | |
canvas.addEventListener('click', onDriveWheelClicked); | |
//canvas.addEventListener('mozfullscreenchange', onFullScreenChange); | |
//canvas.addEventListener('webkitfullscreenchange', onFullScreenChange); | |
//requestFullScreen(canvas); called when event is triggered | |
//https://stackoverflow.com/questions/13157586/full-screen-canvas-on-mobile-devices | |
function requestFullScreen(el) { | |
if (el.webkitRequestFullScreen) { | |
// Google Chrome Version 73.0.3683.86 (Official Build) (32-bit) | |
//Failed to execute 'requestFullscreen' on 'Element': API can only be initiated by a user gesture. | |
el.webkitRequestFullScreen(); | |
} else { | |
el.mozRequestFullScreen(); | |
} | |
} | |
var context = canvas.getContext('2d'); | |
var count = wheels.length; | |
var centerAngle = 2 * Math.PI / count; | |
var cx = canvas.width / 2, | |
cy = canvas.height / 2; | |
var radius = 120; | |
// calculate the starting angle to make it symmetric with respect to the y-axis | |
//But draw piles clockwise from the positive y axis (12 oclock). | |
angle = count % 2 ? Math.PI / -2 : Math.PI / -2 - centerAngle / 2; // is odd? | |
centerWheel.rotate = 0; | |
centerWheel.x = cx; | |
centerWheel.y = cy; | |
centerWheel.radius = radius / 2; | |
centerWheel.directionAngle = Math.PI + angle; | |
// https://www.mathsisfun.com/geometry/regular-polygons.html | |
for (var idxPie = 0; idxPie < count; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
//angle = startAngle + (idx * centerAngle); | |
var vx = cx + radius * Math.cos(angle); | |
var vy = cy + radius * Math.sin(angle); | |
wheel.direction = idxPie; | |
wheel.name = String.fromCharCode(65+idxPie); | |
wheel.x = vx; | |
wheel.y = vy; | |
wheel.radius = radius / 2; | |
wheel.rotate = 0; //Math.PI + idxPie * centerAngle. clamp the angle to the range 0 to 1 in turns | |
wheel.directionAngle = angle; | |
drawPie(context, wheel, 'black', angle); | |
} | |
drawPie(context, centerWheel, 'black'); | |
requestAnimationFrame(animate); | |
/** | |
* @param {MouseEvent} me | |
* @see https://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element | |
*/ | |
function onDriveWheelClicked(me) { | |
// Modern browser's now handle this for you. Chrome, IE9, and Firefox support the offsetX/Y | |
// like this, passing in the event from the click handler. | |
var x = me.offsetX, | |
y = me.offsetY; | |
// if (me.pageX || me.pageY) { | |
// x = me.pageX; | |
// y = me.pageY; | |
// } else { | |
// x = me.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; | |
// y = me.clientY + document.body.scrollTop + document.documentElement.scrollTop; | |
// } | |
// var canvasElement = me.currentTarget; | |
// x -= canvasElement.offsetLeft; | |
// y -= canvasElement.offsetTop; | |
const radian360 = 2 * Math.PI; | |
const pieCount = wheels.length; | |
const deltaAngle = radian360 / pieCount; | |
for (var idxPie = 0; idxPie < pieCount; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
if (Math.pow(x - wheel.x, 2) + Math.pow(y - wheel.y, 2) <= Math.pow(wheel.radius, 2)) { | |
//var rotate = wheel.rotate + deltaAngle; //clockwise | |
//wheel.rotate = rotate % radian360; // clamp the angle to the range 0 to `2 * Math.PI in radians | |
// ClampAngle Limit the angle to the range 0 to 1 in turns | |
//rotate = centerWheel.rotate - deltaAngle; // Counterclockwise | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
//centerWheel.rotate = (rotate % radian360) + (rotate < 0 ? radian360 : 0); | |
startRotateByAction(wheel, deltaAngle); | |
startRotateByAction(centerWheel, -deltaAngle); | |
++steps; | |
} | |
} | |
var event = me || window.event; | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
//https://jlongster.com/Going-Fullscreen-with-Canvas | |
function onFullScreenChange(evt) { | |
var canvas = evt.currentTarget; | |
if (document.mozFullScreen || document.webkitIsFullScreen) { | |
var rect = canvas.getBoundingClientRect(); | |
canvas.width = rect.width; | |
canvas.height = rect.height; | |
} else { | |
canvas.width = 500; | |
canvas.height = 400; | |
} | |
} | |
var lastTime; | |
function animate(timestamp) { | |
context.deltaTime = lastTime ? timestamp - lastTime : 0; | |
lastTime = timestamp; | |
//The clearRect() method sets the pixels in a rectangular area to transparent black (rgba(0,0,0,0)). | |
// context.clearRect(0, 0, canvas.width, canvas.height); | |
context.fillStyle = '#E3E8E6'; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
drawPie(context, centerWheel, 'black'); | |
//var logs = []; | |
var score = 0; | |
var count = centerWheel.length; | |
var centralAngle = 2 * Math.PI / count; | |
var angle = count % 2 ? Math.floor(count / 2) * centralAngle - Math.PI / 2 : centralAngle / 2; // is odd? | |
for (var idx = 0; idx < count; ++idx) { | |
// `center.rotate` is a normalized angle clamped in [0, 2 * Math.PI), so `index` is an integer in the range [0, count). | |
var index = Math.floor((centerWheel.rotate) / centralAngle); // direction0's (the first direction) index in center colors; | |
index -= idx; // the idx-th direction's index in center colors; | |
if (index < 0) { | |
index += count; | |
} | |
var wheel = wheels[idx]; | |
// Retrieves the index of the slice pointed to the center wheel | |
var pointer = Math.floor(wheel.rotate / centralAngle); //The array index of the segment sector pointed to the center | |
var matched = centerWheel[index] == wheel[pointer]; | |
wheel.pointer = matched ? pointer : null; | |
if (matched) { | |
++score; | |
} | |
//logs.push(`direction_${idx}: ${index}=${centerWheel[index]} vs ${pointer}=${wheel[pointer]}`); | |
drawPie(context, wheel, 'black', angle); | |
angle += centralAngle; | |
} | |
//console.log(logs.join(', ')); | |
//https://www.tombraiderchronicles.com/underworld/walkthrough/level02.html#pastthedoorpuzzle | |
if (count === score) { | |
context.font = context.font.replace(/\d+px/, '56px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'top'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('You win!', canvas.width / 2, 1); | |
} | |
//draw the number of steps | |
context.font = context.font.replace(/\d+px/, '28px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'bottom'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('Steps: ' + steps, canvas.width / 2, canvas.height - 1); | |
requestAnimationFrame(animate); | |
} | |
} | |
startLoop(); | |
//initWheels(wheels); | |
//console.log( wheels ); | |
//console.log(getIndex(wheels[0])); | |
/** | |
* https://developer.mozilla.org/en-US/docs/Web/CSS/conic-gradient#Customizing_Gradients | |
* @see https://github.com/leaverou/conic-gradient | |
*/ | |
//element.style.maxWidth = "100px"; | |
//transform: rotate(45deg); | |
//ELEMENT.style.setProperty('--element-width', NEW_VALUE); | |
//pieChart(wheel); | |
// Center X and center Y are the coordinates of the center point of the polygon. | |
// Set initially to 550, 550. Note that the y coordinate is positive downwards, | |
// to conform to the convention in most computer software. Positive x is to the right. | |
// Start angle (degrees): Start angle is the position of the first vertex. | |
// This angle is in degrees and is the angle starting at 3 o'clock going counter | |
// clockwise. So for example if you want the first vertex to be at 12 o'clock, set | |
// this to 90. Set initially to blank (auto). | |
// If you leave this blank it will be set automatically: If the number of sides is | |
// odd, (e.g. a pentagon), the first vertex will be at 12 o'clock. If even, e.g. an | |
// octagon, the top and bottom sides will be horizontal on the page. | |
/** | |
* This calculator takes the parameters of a regular polygon and calculates its coordinates. | |
* It produces both the coordinates of the vertices and the coordinates of the line segments | |
* making up the sides of the polygon. | |
* | |
* Note that the y coordinates are positive downwards, to conform to the convention in most | |
* computer software. Positive x is to the right. | |
* | |
* @param {number} nsides The number of sides. Must be greater than 2. Set initially to 5. | |
* @param {number} radius The distance from the center to a vertex. Set initially to 100. | |
* @param {number} startAngle | |
*/ | |
function calculatePolygonCoordinates(nsides, radius, startAngle, cx, cy) { | |
// collect inputs | |
// if(isNaN(radius) || radius<1) { alert("Radius must be a number greater than 0"); return; } | |
// if(isNaN(n) || n<3) { alert("Number of sides must be a number greater than 2"); return; } | |
// if(isNaN(startAng)) { alert("Starting angle must be a number"); return; } | |
var centerAngle = 2 * Math.PI / nsides; | |
//calculate the default start angle | |
if (!startAngle) { //none supplied | |
if (isOdd(nsides)) | |
startAngle = Math.PI / 2; //12 oclock | |
else | |
startAngle = Math.PI / 2 - centerAngle / 2; | |
} | |
var angle = startAngle, | |
vertex = []; // new Array(); | |
for (var idx = 0; idx < nsides; idx++, angle += centerAngle) { | |
//angle = startAngle + (idx * centerAngle); | |
var vx = Math.round(cx + radius * Math.cos(angle)); | |
var vy = Math.round(cy - radius * Math.sin(angle)); | |
vertex.push({ | |
x: vx, | |
y: vy | |
}); | |
} | |
function isOdd(n) { | |
return (n % 2 == 1); | |
} | |
function toRadians(degs) { | |
return Math.PI * degs / 180; | |
} | |
return vertex; | |
} | |
</script></body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
margin: 0px; | |
padding: 0px; | |
width: 100%; | |
} | |
/* https://stackoverflow.com/questions/10614481/disable-double-tap-zoom-option-in-browser-on-touch-devices */ | |
.disable-dbl-tap-zoom, .mycanvas { | |
touch-action: manipulation; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
try{ top.document.getElementById('codefund_ad').remove(); } catch(e){ console.log(e); } | |
/** | |
* @param {string} html Text that contains text and tags to be converted to a document fragment. e.g. '<div>x</div><span>y</span><br/>' | |
* @return {DocumentFragment} a minimal document object that has no parent (a lightweight version of `Document`) | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/Range/selectNodeContents | |
* @see https://developer.mozilla.org/en-US/docs/Web/API/range/createContextualFragment | |
* @example document.body.appendChild($j('<div>x</div><span>y</span><br/>')); | |
* @since IE11 | |
*/ | |
function $j(html) { | |
/// Way 1 | |
//var temp = document.createElement('template'); | |
//temp.innerHTML = html; | |
//return temp.content; | |
/// Way 2 | |
// var range = document.createRange(); | |
// Set body as context node or whatever context the fragment is to be evaluated in. | |
// range.selectNodeContents(document.body); // range.selectNode(document.body); | |
// return range.createContextualFragment(html); | |
return document.createRange().createContextualFragment(html); | |
} | |
/** | |
* @typedef Wheel | |
* @type {string[] | {x: number, y: number, radius: number, rotate: number, pointer: number, direction: number, originAngle: number}} | |
* @property {number} x | |
* @property {number} y | |
* @property {number} radius | |
* @property {number} rotate | |
* @property {number} direction | |
* @property {number} originAngle the starting angle to draw this wheel | |
* @property {number|null} pointer The array index of the segment sector pointed to the center | |
* @property {string} name | |
*/ | |
/** @type {Wheel} init each array clockwise */ | |
var centerWheel = ['red', 'green', 'gray', 'gold', 'pink', 'purple']; // Idler wheel / driven wheel | |
/** | |
* @type {Wheel[]} Each of the top points of the 4 smaller stars are rotated such that they point towards the center point of the larger star. | |
* init each array counterclockwise and these wheels rotate around their center clockwise | |
* @see https://en.wikipedia.org/wiki/Regular_polygon#Regular_convex_polygons | |
*/ | |
var wheels = [ //Drive wheels | |
['red', 'gold', 'red', 'green', 'pink', 'purple'], | |
['gray', 'gold', 'gray', 'red', 'pink', 'purple'], | |
['gray', 'gray', 'green', 'gray', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'pink', 'purple'], | |
['gold', 'green', 'gray', 'red', 'purple', 'purple'] | |
]; | |
//centerWheel.length = 4; | |
//wheels.length = 4; | |
//wheels.forEach(function (it){ it.length=4; }); | |
/** | |
* Draw an isosceles triangle as an arrowhead | |
``` | |
|\ | |
+--------------------------+ \ | |
| > end (locx, locy) | |
+--------------------------+ / | |
|/ | |
|<->| sizey | |
``` | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {number} locx The x axis of the coordinate for the end of the arrow | |
* @param {number} locy The y axis of the coordinate for the end of the arrow | |
* @param {number} angle | |
* @param {number} sizex The remaining/base/hem side's length, not the two equal sides (legs). | |
* @param {number} sizey The Height, i.e. the distance from top to bottom. Or at right angles | |
* from any base to the furthest corner. | |
* @see https://stackoverflow.com/questions/6576827/html-canvas-draw-curved-arrows#6577443 | |
* @see https://github.com/frogcat/canvas-arrow | |
* @see https://en.wikipedia.org/wiki/Arrow#Arrowhead | |
* @see https://en.wikipedia.org/wiki/Isosceles_triangle | |
*/ | |
function drawArrowhead(ctx, locx, locy, angle, sizex, sizey) { | |
var hx = sizex / 2; | |
var hy = sizey / 2; | |
ctx.save(); | |
ctx.translate(locx, locy); | |
ctx.rotate(angle); | |
ctx.translate(-hx, -hy); | |
ctx.beginPath(); | |
ctx.moveTo(0, 0); | |
ctx.lineTo(0, sizey); | |
ctx.lineTo(sizex, hy); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.restore(); | |
} | |
/** | |
* | |
* @param {CanvasRenderingContext2D} ctx | |
* @param {string[]&{x:number, y:number, radius:number}} wheel | |
* @param {string | CanvasGradient | CanvasPattern} strokeStyle | |
* @param {number} directionAngle The angle of the line of the center wheel and this wheel's centers, | |
* measured clockwise around the center wheel from the positive x-axis and expressed in radians. | |
*/ | |
function drawPie(ctx, wheel, strokeStyle) { | |
if(wheel.action){ | |
wheel.action.act(ctx.deltaTime, wheel); | |
} | |
const lineWidth = 1; | |
const innerRadius = 15; | |
const innerStartRadius = innerRadius - lineWidth / 2; | |
ctx.lineWidth = lineWidth; | |
var slices = wheel.length; | |
var sliceAngle = Math.PI / slices; | |
//https://en.wikipedia.org/wiki/Angular_distance // | |
var spaceAngle = sliceAngle; //the angle between the end side of the slice and the start side of the next adjacent slice. | |
for (var idxSlice = 0, angle = wheel.directionAngle + Math.PI - sliceAngle / 2 + wheel.rotate; idxSlice < slices; ++idxSlice) { | |
// Begin a path as the segment consists of an arc and 2 lines. | |
ctx.beginPath(); | |
if (null == wheel.direction) { | |
ctx.moveTo(wheel.x, wheel.y); // ctx.moveTo(centerX, centerY); | |
} else { | |
// Work out the x and y values for the starting point of the segment which is at its starting angle | |
// but out from the center point of the wheel by the value of the innerRadius. Some correction for line width is needed. | |
// Now move here relative to the center point of the wheel. | |
ctx.moveTo(wheel.x + Math.cos(angle) * innerStartRadius, wheel.y + Math.sin(angle) * innerStartRadius); | |
} | |
// Draw the outer arc of the segment clockwise in direction --> | |
// `startAngle`: The angle at which the arc starts, measured clockwise from the positive x-axis and expressed in radians. | |
ctx.arc(wheel.x, wheel.y, wheel.radius, angle, angle + sliceAngle); | |
if (null != wheel.direction) { | |
// Draw another arc, this time anticlockwise <-- at the innerRadius between the end angle and the start angle. | |
// Canvas will draw a connecting line from the end of the outer arc to the beginning of the inner arc completing the shape. | |
ctx.arc(wheel.x, wheel.y, innerRadius, angle + sliceAngle, angle, true); | |
} else { | |
// If no inner radius then we draw a line back to the center of the wheel. | |
// Draw a line back to the center of the wheel. `closePath` automatically connects the shape's first and last points. | |
//ctx.closePath(); ctx.lineTo(centerX, centerY); | |
} | |
ctx.closePath(); | |
ctx.fillStyle = wheel[idxSlice]; | |
// Fill and stroke the segment. | |
if (idxSlice === wheel.pointer) { | |
var shadowColor = ctx.shadowColor; | |
//https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur | |
//https://jsbin.com/gavepub/edit?html,js,output | |
ctx.shadowBlur = 25; | |
ctx.shadowColor = 'blue'; | |
//Shadows are only drawn if the shadowColor property is set to a non-transparent value. One of the | |
//shadowBlur, shadowOffsetX, or shadowOffsetY properties must be non-zero, as well. | |
//ctx.shadowOffsetX = 0; // The default value is 0 (no vertical offset). | |
//ctx.shadowOffsetY = 0; // The default value is 0 (no vertical offset). | |
ctx.fill(); | |
ctx.strokeStyle = 'blue'; | |
ctx.stroke(); | |
ctx.shadowBlur = 0; //The default value is 0. | |
ctx.shadowColor = shadowColor; //The default value is fully-transparent black 'rgba(0, 0, 0, 0)' | |
} else { | |
ctx.fill(); | |
ctx.strokeStyle = strokeStyle; | |
ctx.stroke(); | |
} | |
// only for debug test | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "white"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
var halfwayAngle = angle + sliceAngle / 2; // symmetry axis | |
var textRadius = 0.8 * wheel.radius; | |
ctx.fillText(idxSlice, wheel.x + textRadius * Math.cos(halfwayAngle), wheel.y + textRadius * Math.sin(halfwayAngle)); | |
angle -= sliceAngle + spaceAngle; | |
} | |
// draw Circle border | |
ctx.beginPath(); | |
ctx.arc(wheel.x, wheel.y, wheel.radius, 0, Math.PI * 2); | |
ctx.strokeStyle = strokeStyle; | |
ctx.lineWidth = 2; | |
ctx.stroke(); | |
if (null != wheel.direction) { | |
// draw the rotation direction indicator | |
ctx.beginPath(); | |
ctx.fillStyle = 'rgba(55, 217, 56,1)'; | |
ctx.strokeStyle = 'rgba(55, 217, 56,1)'; | |
var endAngle = wheel.directionAngle + Math.PI / 10; | |
ctx.arc(wheel.x, wheel.y, wheel.radius + 8, wheel.directionAngle - Math.PI / 10, endAngle); | |
ctx.stroke(); | |
var lastX = wheel.x + (wheel.radius + 8) * Math.cos(endAngle); | |
var lastY = wheel.y + (wheel.radius + 8) * Math.sin(endAngle); | |
// var ang = findAngle(sx, sy, ex, ey); | |
// ctx.fillRect(lastX, lastY, 4, 4); | |
drawArrowhead(ctx, lastX, lastY, Math.PI / 2 + endAngle, 12, 12); | |
} | |
// only for debug test | |
if (null != wheel.name) { | |
// https://stackoverflow.com/questions/18092753/change-font-size-of-canvas-without-knowing-font-family | |
ctx.font = ctx.font.replace(/\d+px/, '24px'); | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = 'middle'; | |
ctx.fillStyle = "cyan"; | |
// var textMetrics = ctx.measureText(this.x); // TextMetrics object | |
// ctx.fillText(wheel.x, wheel.y - textMetrics.width / 2, oy); | |
ctx.fillText(wheel.name, wheel.x, wheel.y); | |
} | |
// if (null != wheel.pointer) { | |
// ctx.fillStyle = 'blue'; | |
// ctx.fillText(wheel.pointer, wheel.x + 38, wheel.y); | |
// } | |
} | |
function startRotateByAction(wheel, delta) { | |
var action; | |
if(wheel.action){ | |
action = wheel.action; | |
action.finish(wheel); | |
} else{ | |
action = {}; | |
wheel.action = action; | |
} | |
action.time = 0; | |
action.duration = 500; | |
action.start = wheel.rotate; | |
action.delta = delta; | |
action.finish = function (wheel){ | |
const radian360 = 2 * Math.PI; | |
const endAngle = this.start + this.delta; | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
wheel.rotate = (endAngle % radian360) + (endAngle < 0 ? radian360 : 0); | |
}; | |
action.act = function(interval, wheel){ | |
if(this.time < this.duration){ | |
this.time += interval; | |
wheel.rotate = this.start + this.time/this.duration * this.delta; | |
} else { | |
this.finish(wheel); | |
wheel.action = null; | |
} | |
}; | |
} | |
function startLoop() { | |
var steps = 0; // counts the rotation numbers, number of revolution | |
/** @type {HTMLCanvasElement} */ | |
var canvas = document.getElementById('mycanvas'); | |
if (!canvas) { | |
canvas = document.createElement('canvas'); | |
canvas.className = 'mycanvas'; | |
document.body.appendChild(canvas); | |
} | |
canvas.addEventListener('click', onDriveWheelClicked); | |
//canvas.addEventListener('mozfullscreenchange', onFullScreenChange); | |
//canvas.addEventListener('webkitfullscreenchange', onFullScreenChange); | |
//requestFullScreen(canvas); called when event is triggered | |
//https://stackoverflow.com/questions/13157586/full-screen-canvas-on-mobile-devices | |
function requestFullScreen(el) { | |
if (el.webkitRequestFullScreen) { | |
// Google Chrome Version 73.0.3683.86 (Official Build) (32-bit) | |
//Failed to execute 'requestFullscreen' on 'Element': API can only be initiated by a user gesture. | |
el.webkitRequestFullScreen(); | |
} else { | |
el.mozRequestFullScreen(); | |
} | |
} | |
var context = canvas.getContext('2d'); | |
var count = wheels.length; | |
var centerAngle = 2 * Math.PI / count; | |
var cx = canvas.width / 2, | |
cy = canvas.height / 2; | |
var radius = 120; | |
// calculate the starting angle to make it symmetric with respect to the y-axis | |
//But draw piles clockwise from the positive y axis (12 oclock). | |
angle = count % 2 ? Math.PI / -2 : Math.PI / -2 - centerAngle / 2; // is odd? | |
centerWheel.rotate = 0; | |
centerWheel.x = cx; | |
centerWheel.y = cy; | |
centerWheel.radius = radius / 2; | |
centerWheel.directionAngle = Math.PI + angle; | |
// https://www.mathsisfun.com/geometry/regular-polygons.html | |
for (var idxPie = 0; idxPie < count; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
//angle = startAngle + (idx * centerAngle); | |
var vx = cx + radius * Math.cos(angle); | |
var vy = cy + radius * Math.sin(angle); | |
wheel.direction = idxPie; | |
wheel.name = String.fromCharCode(65+idxPie); | |
wheel.x = vx; | |
wheel.y = vy; | |
wheel.radius = radius / 2; | |
wheel.rotate = 0; //Math.PI + idxPie * centerAngle. clamp the angle to the range 0 to 1 in turns | |
wheel.directionAngle = angle; | |
drawPie(context, wheel, 'black', angle); | |
} | |
drawPie(context, centerWheel, 'black'); | |
requestAnimationFrame(animate); | |
/** | |
* @param {MouseEvent} me | |
* @see https://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element | |
*/ | |
function onDriveWheelClicked(me) { | |
// Modern browser's now handle this for you. Chrome, IE9, and Firefox support the offsetX/Y | |
// like this, passing in the event from the click handler. | |
var x = me.offsetX, | |
y = me.offsetY; | |
// if (me.pageX || me.pageY) { | |
// x = me.pageX; | |
// y = me.pageY; | |
// } else { | |
// x = me.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; | |
// y = me.clientY + document.body.scrollTop + document.documentElement.scrollTop; | |
// } | |
// var canvasElement = me.currentTarget; | |
// x -= canvasElement.offsetLeft; | |
// y -= canvasElement.offsetTop; | |
const radian360 = 2 * Math.PI; | |
const pieCount = wheels.length; | |
const deltaAngle = radian360 / pieCount; | |
for (var idxPie = 0; idxPie < pieCount; ++idxPie, angle += centerAngle) { | |
var wheel = wheels[idxPie]; | |
if (Math.pow(x - wheel.x, 2) + Math.pow(y - wheel.y, 2) <= Math.pow(wheel.radius, 2)) { | |
//var rotate = wheel.rotate + deltaAngle; //clockwise | |
//wheel.rotate = rotate % radian360; // clamp the angle to the range 0 to `2 * Math.PI in radians | |
// ClampAngle Limit the angle to the range 0 to 1 in turns | |
//rotate = centerWheel.rotate - deltaAngle; // Counterclockwise | |
//https://stackoverflow.com/questions/1628386/normalise-orientation-between-0-and-360 | |
//centerWheel.rotate = (rotate % radian360) + (rotate < 0 ? radian360 : 0); | |
startRotateByAction(wheel, deltaAngle); | |
startRotateByAction(centerWheel, -deltaAngle); | |
++steps; | |
} | |
} | |
var event = me || window.event; | |
event.preventDefault(); | |
event.stopPropagation(); | |
} | |
//https://jlongster.com/Going-Fullscreen-with-Canvas | |
function onFullScreenChange(evt) { | |
var canvas = evt.currentTarget; | |
if (document.mozFullScreen || document.webkitIsFullScreen) { | |
var rect = canvas.getBoundingClientRect(); | |
canvas.width = rect.width; | |
canvas.height = rect.height; | |
} else { | |
canvas.width = 500; | |
canvas.height = 400; | |
} | |
} | |
var lastTime; | |
function animate(timestamp) { | |
context.deltaTime = lastTime ? timestamp - lastTime : 0; | |
lastTime = timestamp; | |
//The clearRect() method sets the pixels in a rectangular area to transparent black (rgba(0,0,0,0)). | |
// context.clearRect(0, 0, canvas.width, canvas.height); | |
context.fillStyle = '#E3E8E6'; | |
context.fillRect(0, 0, canvas.width, canvas.height); | |
drawPie(context, centerWheel, 'black'); | |
//var logs = []; | |
var score = 0; | |
var count = centerWheel.length; | |
var centralAngle = 2 * Math.PI / count; | |
var angle = count % 2 ? Math.floor(count / 2) * centralAngle - Math.PI / 2 : centralAngle / 2; // is odd? | |
for (var idx = 0; idx < count; ++idx) { | |
// `center.rotate` is a normalized angle clamped in [0, 2 * Math.PI), so `index` is an integer in the range [0, count). | |
var index = Math.floor((centerWheel.rotate) / centralAngle); // direction0's (the first direction) index in center colors; | |
index -= idx; // the idx-th direction's index in center colors; | |
if (index < 0) { | |
index += count; | |
} | |
var wheel = wheels[idx]; | |
// Retrieves the index of the slice pointed to the center wheel | |
var pointer = Math.floor(wheel.rotate / centralAngle); //The array index of the segment sector pointed to the center | |
var matched = centerWheel[index] == wheel[pointer]; | |
wheel.pointer = matched ? pointer : null; | |
if (matched) { | |
++score; | |
} | |
//logs.push(`direction_${idx}: ${index}=${centerWheel[index]} vs ${pointer}=${wheel[pointer]}`); | |
drawPie(context, wheel, 'black', angle); | |
angle += centralAngle; | |
} | |
//console.log(logs.join(', ')); | |
//https://www.tombraiderchronicles.com/underworld/walkthrough/level02.html#pastthedoorpuzzle | |
if (count === score) { | |
context.font = context.font.replace(/\d+px/, '56px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'top'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('You win!', canvas.width / 2, 1); | |
} | |
//draw the number of steps | |
context.font = context.font.replace(/\d+px/, '28px'); | |
context.textAlign = 'center'; | |
context.textBaseline = 'bottom'; | |
context.fillStyle = 'rgba(55, 217, 56,1)'; | |
context.fillText('Steps: ' + steps, canvas.width / 2, canvas.height - 1); | |
requestAnimationFrame(animate); | |
} | |
} | |
startLoop(); | |
//initWheels(wheels); | |
//console.log( wheels ); | |
//console.log(getIndex(wheels[0])); | |
/** | |
* https://developer.mozilla.org/en-US/docs/Web/CSS/conic-gradient#Customizing_Gradients | |
* @see https://github.com/leaverou/conic-gradient | |
*/ | |
//element.style.maxWidth = "100px"; | |
//transform: rotate(45deg); | |
//ELEMENT.style.setProperty('--element-width', NEW_VALUE); | |
//pieChart(wheel); | |
// Center X and center Y are the coordinates of the center point of the polygon. | |
// Set initially to 550, 550. Note that the y coordinate is positive downwards, | |
// to conform to the convention in most computer software. Positive x is to the right. | |
// Start angle (degrees): Start angle is the position of the first vertex. | |
// This angle is in degrees and is the angle starting at 3 o'clock going counter | |
// clockwise. So for example if you want the first vertex to be at 12 o'clock, set | |
// this to 90. Set initially to blank (auto). | |
// If you leave this blank it will be set automatically: If the number of sides is | |
// odd, (e.g. a pentagon), the first vertex will be at 12 o'clock. If even, e.g. an | |
// octagon, the top and bottom sides will be horizontal on the page. | |
/** | |
* This calculator takes the parameters of a regular polygon and calculates its coordinates. | |
* It produces both the coordinates of the vertices and the coordinates of the line segments | |
* making up the sides of the polygon. | |
* | |
* Note that the y coordinates are positive downwards, to conform to the convention in most | |
* computer software. Positive x is to the right. | |
* | |
* @param {number} nsides The number of sides. Must be greater than 2. Set initially to 5. | |
* @param {number} radius The distance from the center to a vertex. Set initially to 100. | |
* @param {number} startAngle | |
*/ | |
function calculatePolygonCoordinates(nsides, radius, startAngle, cx, cy) { | |
// collect inputs | |
// if(isNaN(radius) || radius<1) { alert("Radius must be a number greater than 0"); return; } | |
// if(isNaN(n) || n<3) { alert("Number of sides must be a number greater than 2"); return; } | |
// if(isNaN(startAng)) { alert("Starting angle must be a number"); return; } | |
var centerAngle = 2 * Math.PI / nsides; | |
//calculate the default start angle | |
if (!startAngle) { //none supplied | |
if (isOdd(nsides)) | |
startAngle = Math.PI / 2; //12 oclock | |
else | |
startAngle = Math.PI / 2 - centerAngle / 2; | |
} | |
var angle = startAngle, | |
vertex = []; // new Array(); | |
for (var idx = 0; idx < nsides; idx++, angle += centerAngle) { | |
//angle = startAngle + (idx * centerAngle); | |
var vx = Math.round(cx + radius * Math.cos(angle)); | |
var vy = Math.round(cy - radius * Math.sin(angle)); | |
vertex.push({ | |
x: vx, | |
y: vy | |
}); | |
} | |
function isOdd(n) { | |
return (n % 2 == 1); | |
} | |
function toRadians(degs) { | |
return Math.PI * degs / 180; | |
} | |
return vertex; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment