Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active March 29, 2023 10:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vasturiano/d1441fc9b1a5cf9e212bd966d959589b to your computer and use it in GitHub Desktop.
Save vasturiano/d1441fc9b1a5cf9e212bd966d959589b to your computer and use it in GitHub Desktop.
Spinning Wheel

Simulation of the visual illusion that occurs when the visual capture frame-rate is unable to absorb all the intermediary positions of a spinning object. Patterns start emerging at angular velocities that are multiples of even circle divisions.

The wheel's angular velocity increases linearly from 0 to infinity. Its constant acceleration is implemented using the force-constant force type, part of the d3-force simulation engine.

<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.js"></script>
<script src="//unpkg.com/d3-force-constant"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="controls">
# spokes:
<input type="number" min="1" max="60" step="1" value="1" oninput="onSpokesChange(this.value)">
</div>
<div class="info-section">
Angular velocity: <span class="angle-info"></span>&deg;/frame
<br>
(<span class="freq-info"></span> Hz)
</div>
<svg id="canvas"></svg>
<script src="index.js"></script>
</body>
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)`);
}
body {
margin: 0;
text-align: center;
font-family: sans-serif;
font-size: 14px;
}
.wheel {
stroke: midnightblue;
stroke-width: 4px;
fill: none;
}
.spoke {
stroke: darkslategrey;
stroke-width: 8px;
stroke-linecap:round;
}
#controls {
position: absolute;
margin: 8px;
}
.info-section {
position: absolute;
text-align: center;
bottom: 6px;
margin: auto;
left: 50%;
transform: translate(-50%, 0);
}
.freq-info {
width: 35px;
display: inline-block;
text-align: center;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment