const ACCELERATION = 0.05; // degrees/tick^2 const width = window.innerWidth; const height = window.innerHeight; const radius = Math.min(width, height) * 0.4; // Dom init const canvas = d3.select('svg#canvas') .attr('width', width) .attr('height', height) .attr('viewBox', `${-width/2} ${-height/2} ${width} ${height}`); canvas.append('circle') .attr('class', 'wheel') .attr('r', radius); const spokesG = canvas.append('g'); onSpokesChange(1); // Initialize with one spoke // Set up simulation const angleNode = { x: 0, y: 0 }; let prevTs = new Date(); let prevAngle = 0; const sim = d3.forceSimulation() .nodes([angleNode]) // single node .alphaDecay(0) .velocityDecay(0) .force('spin', d3.forceConstant() .direction(0) //affect only x .strength(ACCELERATION) ) .on('tick', () => { spokesG.attr('transform', `rotate(${angleNode.x} 0 0)`); // Loggers const angle = sim.nodes()[0].x; d3.select('.angle-info') .text(Math.round(sim.nodes()[0].vx)); const now = new Date(); d3.select('.freq-info') .text(Math.round((angle-prevAngle)/360 / ((now-prevTs)/1000) * 10) / 10); prevTs = now; prevAngle = angle; }); // function onSpokesChange(numSpokes) { if (numSpokes <= 0) return; const spokeAngles = d3.range(numSpokes).map(n => n/numSpokes * 360); const spoke = spokesG.selectAll('line.spoke') .data(spokeAngles); spoke.exit().remove(); spoke.merge( spoke.enter().append('line') .attr('class', 'spoke') .attr('x1', 0) .attr('x2', radius) .attr('y1', 0) .attr('y2', 0) ) .attr('transform', d => `rotate(${d} 0 0)`); }