this.ref = ref} className={`idyll-step ${className || ''}`} {...props} />
13 | );
14 | }
15 | }
16 |
17 | module.exports = Step;
18 |
--------------------------------------------------------------------------------
/components/default/stepper-control.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class StepperControl extends React.Component {
4 |
5 | componentDidMount() {
6 | }
7 | render() {
8 | const { idyll, ...props } = this.props;
9 | return
this.ref = ref} className={`idyll-step ${className || ''}`} style={{margin: '10vh 0 60vh 0'}} {...props} />
21 | // );
22 | }
23 | }
24 |
25 | module.exports = StepperControl;
26 |
--------------------------------------------------------------------------------
/components/default/stepper.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getProperty } from 'idyll-ast';
3 | const { filterChildren, mapChildren } = require('idyll-component-children');
4 |
5 | const Step = require('./step');
6 |
7 | class Stepper extends React.PureComponent {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.SCROLL_STEP_MAP = {};
12 | this.SCROLL_NAME_MAP = {};
13 | }
14 |
15 |
16 | registerStep(elt, name, val) {
17 | this.SCROLL_STEP_MAP[elt] = val;
18 | this.SCROLL_NAME_MAP[elt] = name;
19 | }
20 |
21 | getSteps() {
22 | return filterChildren(
23 | this.props.children,
24 | (c) => {
25 | return c.type.name && c.type.name.toLowerCase() === 'step';
26 | }
27 | )
28 | }
29 |
30 | next() {
31 | this.props.updateProps({ currentStep: (this.props.currentStep + 1) % (this.getSteps().length) });
32 | }
33 | previous() {
34 | let newStep = this.props.currentStep - 1;
35 | if (newStep < 0) {
36 | newStep = (this.getSteps().length) + newStep;
37 | }
38 |
39 | this.props.updateProps({ currentStep: newStep });
40 | }
41 |
42 | getSelectedStep() {
43 | const { currentState, currentStep } = this.props;
44 | const steps = this.getSteps();
45 | if (currentState) {
46 | return filterChildren(
47 | steps,
48 | (c) => {
49 | return c.props.state === currentState
50 | }
51 | )[0];
52 | }
53 | return steps[currentStep % steps.length];
54 | }
55 |
56 | render() {
57 | const { children, height, ...props } = this.props;
58 | return (
59 |
60 |
61 | {filterChildren(
62 | children,
63 | (c) => {
64 | return c.type.name && c.type.name.toLowerCase() === 'graphic';
65 | }
66 | )}
67 |
68 |
69 | {
70 | mapChildren(this.getSelectedStep(), (c) => {
71 | return React.cloneElement(c, {
72 | registerStep: this.registerStep.bind(this)
73 | })
74 | })
75 | }
76 |
77 | {mapChildren(filterChildren(
78 | children,
79 | (c) => {
80 | return c.type.name && c.type.name.toLowerCase() === 'steppercontrol';
81 | }
82 | ), (c) => {
83 | return React.cloneElement(c, {
84 | next: this.next.bind(this),
85 | previous: this.previous.bind(this)
86 | })
87 | })}
88 |
89 | );
90 | }
91 | }
92 |
93 |
94 | Stepper.defaultProps = {
95 | currentStep: 0,
96 | height: 500
97 | };
98 |
99 | Stepper._idyll = {
100 | name: "Stepper",
101 | tagType: "open",
102 | children: [`
103 | [Step]This is the content for step 1[/Step]
104 | [Step]This is the content for step 2[/Step]
105 | [Step]This is the content for step 3[/Step]`],
106 | props: [{
107 | name: "currentStep",
108 | type: "number",
109 | example: '0'
110 | }]
111 | }
112 | export default Stepper;
113 |
--------------------------------------------------------------------------------
/components/default/svg.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import InlineSVG from 'react-inlinesvg';
3 |
4 | class SVG extends React.PureComponent {
5 | render() {
6 | return (
7 |
8 | );
9 | }
10 | }
11 |
12 | SVG.defaultProps = {
13 | src: ''
14 | }
15 |
16 | SVG._idyll = {
17 | name: "SVG",
18 | tagType: "closed",
19 | props: [{
20 | name: "src",
21 | type: "string",
22 | example: '"https://upload.wikimedia.org/wikipedia/commons/f/fd/Ghostscript_Tiger.svg"'
23 | }]
24 | }
25 |
26 | export default SVG;
27 |
28 |
--------------------------------------------------------------------------------
/components/default/table.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const Table = require('react-table').default;
3 |
4 | class TableComponent extends React.PureComponent {
5 | getColumns() {
6 | if (this.props.columns) {
7 | if (this.props.columns.length && typeof this.props.columns[0] === 'string') {
8 | return this.props.columns.map((d) => {
9 | return {
10 | Header: d,
11 | accessor: d
12 | };
13 | })
14 | }
15 |
16 | return this.props.columns;
17 | }
18 | if ((this.props.data || []).length) {
19 | return Object.keys(this.props.data[0]).map((d) => {
20 | return {
21 | Header: d,
22 | accessor: d
23 | }
24 | })
25 | }
26 |
27 | return [];
28 | }
29 | render() {
30 | return (
31 |
38 | );
39 | }
40 | }
41 |
42 | TableComponent.defaultProps = {
43 | showPagination: false,
44 | showPageSizeOptions: false,
45 | showPageJump: false
46 | }
47 |
48 | TableComponent._idyll = {
49 | name: "Table",
50 | tagType: "closed",
51 | props: [{
52 | name: "data",
53 | type: "array",
54 | example: 'x'
55 | }, {
56 | name: "showPagination",
57 | type: "boolean",
58 | example: 'false'
59 | }, {
60 | name: "showPageSizeOptions",
61 | type: "boolean",
62 | example: 'false'
63 | }, {
64 | name: "showPageJump",
65 | type: "boolean",
66 | example: 'false'
67 | }]
68 | }
69 |
70 | module.exports = TableComponent;
71 |
--------------------------------------------------------------------------------
/components/default/text-container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class TextContainer extends React.PureComponent {
4 | render() {
5 | const { idyll, children, className, hasError, updateProps, ...props } = this.props;
6 | const { styles, ...layout } = idyll.layout;
7 | const { styles: _, ...theme } = idyll.theme;
8 | const style = { ...layout, ...theme };
9 | const cn = (className || '') + ' idyll-text-container';
10 | return (
11 |
{children}
12 | );
13 | }
14 | }
15 |
16 | export default TextContainer;
17 |
--------------------------------------------------------------------------------
/components/default/text-input.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const ReactDOM = require('react-dom');
3 |
4 | class TextInput extends React.PureComponent {
5 | constructor(props) {
6 | super(props);
7 | this.onChange = this.onChange.bind(this);
8 | }
9 |
10 | onChange(e) {
11 | this.props.updateProps({ value: e.target.value });
12 | }
13 |
14 | render() {
15 | const { idyll, hasError, updateProps, ...props } = this.props;
16 | return (
17 |
18 | );
19 | }
20 | }
21 |
22 | TextInput._idyll = {
23 | name: "TextInput",
24 | tagType: "closed",
25 | props: [{
26 | name: "value",
27 | type: "string",
28 | example: '"Hello"'
29 | }]
30 | }
31 |
32 | export default TextInput;
33 |
--------------------------------------------------------------------------------
/components/default/utils/container.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const ReactDOM = require('react-dom');
3 |
4 | class Container extends React.Component {
5 | constructor (props) {
6 | super(props);
7 |
8 | this.state = {
9 | expandLeft: 0,
10 | expandRight: 0
11 | };
12 |
13 | this.setPosition = this.setPosition.bind(this);
14 | }
15 |
16 | componentDidMount() {
17 | window.addEventListener('resize', this.setPosition);
18 | try {
19 | this.node = ReactDOM.findDOMNode(this)
20 | this.setPosition();
21 | } catch(e) {}
22 | }
23 |
24 | //shouldComponentUpdate (nextProps, nextState) {
25 | //return Math.round(nextState.expandLeft) !== Math.round(this.state.expandLeft) ||
26 | //Math.round(nextState.expandRight) !== Math.round(this.state.expandRight);
27 | //}
28 |
29 | setPosition () {
30 | var expandLeft, expandRight;
31 | var rect = this.node.getBoundingClientRect();
32 | var pageWidth = window.innerWidth;
33 |
34 | if (this.props.fullBleed) {
35 | expandLeft = Infinity;
36 | expandRight = Infinity;
37 | } else {
38 | expandLeft = this.props.expandLeft === undefined ? this.props.expand : this.props.expandLeft;
39 | expandRight = this.props.expandRight === undefined ? this.props.expand : this.props.expandRight;
40 | }
41 |
42 | var left = Math.max(rect.left - expandLeft, this.props.padding);
43 | var right = Math.min(rect.right + expandRight, pageWidth - this.props.padding);
44 |
45 | this.setState({
46 | expandLeft: left - rect.left,
47 | expandRight: rect.right - right
48 | });
49 | }
50 |
51 | render () {
52 | var expandStyle = Object.assign({}, this.props.style || {}, {
53 | marginLeft: this.state.expandLeft,
54 | marginRight: this.state.expandRight
55 | });
56 |
57 | return
61 |
62 | {this.props.children}
63 |
64 |
65 | }
66 | }
67 |
68 | Container.defaultProps = {
69 | padding: 15,
70 | expand: 0,
71 | fullBleed: false
72 | }
73 |
74 | export default Container;
75 |
--------------------------------------------------------------------------------
/components/default/utils/screen.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import Container from './container';
5 |
6 | class Screen extends React.PureComponent {
7 | constructor (props) {
8 | super(props);
9 | }
10 |
11 |
12 | render () {
13 | let overlayStyle = {
14 | position: this.props.display ? this.props.display : 'relative',
15 | zIndex: 1,
16 | width: this.props.fullBleed ? '100%' : undefined,
17 | left: this.props.display === 'fixed' ? 0 : undefined,
18 | pointerEvents: 'none',
19 | transition: 'background 0.5s'
20 | };
21 |
22 | if (this.props.height) {
23 | overlayStyle.minHeight = this.props.height;
24 | } else {
25 | overlayStyle.height = '100vh';
26 | }
27 |
28 | if (this.props.backgroundImage) {
29 | overlayStyle.backgroundImage = 'url(' + this.props.backgroundImage + ')';
30 | overlayStyle.backgroundSize = 'cover';
31 | overlayStyle.backgroundPosition = 'top center';
32 | }
33 |
34 | let contentContainerStyle = Object.assign({
35 | flexDirection: this.props.direction || 'column',
36 | display: 'flex',
37 | height: '100%',
38 | justifyContent: {
39 | center: 'center'
40 | }[this.props.justify] || undefined
41 | }, this.props.contentContainerStyle || {});
42 |
43 | let contentStyle = {
44 | alignSelf: {
45 | left: 'flex-start',
46 | center: 'center',
47 | right: 'flex-end',
48 | stretch: 'stretch'
49 | }[this.props.align] || 'flex-end',
50 | pointerEvents: 'all'
51 | }
52 |
53 | if (this.props.fullBleed) {
54 | return (
55 |
56 |
57 |
58 |
59 |
60 | {this.props.children}
61 |
62 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
70 | return
78 |
79 |
80 |
81 | {this.props.children}
82 |
83 |
84 |
85 |
86 | }
87 | }
88 |
89 | Screen.defaultProps = {
90 | position: 0.5,
91 | padding: 0,
92 | fullBleed: false,
93 | align: 'left',
94 | };
95 |
96 | export default Screen;
97 |
--------------------------------------------------------------------------------
/components/default/waypoint.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import Screen from './utils/screen';
4 |
5 | class Waypoint extends React.PureComponent {
6 | constructor (props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | return
;
12 | }
13 |
14 | }
15 |
16 | Waypoint._idyll = {
17 | name: "Waypoint",
18 | tagType: "open",
19 | props: [{
20 | name: "onEnterView",
21 | type: "event",
22 | example: "`x = true`"
23 | }]
24 | }
25 |
26 | export default Waypoint;
27 |
--------------------------------------------------------------------------------
/components/image.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class CustomComponent extends React.Component {
4 |
5 | componentDidCatch(e) {
6 | console.log(e);
7 | }
8 | render() {
9 | const { hasError, updateProps, children, ...props } = this.props;
10 | return (
11 |
12 | );
13 | }
14 | }
15 |
16 | module.exports = CustomComponent;
--------------------------------------------------------------------------------
/components/multiRiffle.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class multiRiffle extends React.PureComponent {
4 | render() {
5 | const { onClick, hasError, updateProps, iter, points, ...props } = this.props;
6 | return (
7 |
{
8 |
9 | let t = 0;
10 | let lastPoint = this.props.points[this.props.points.length - 1];
11 |
12 | if (lastPoint.y !== 1) {
13 | for (let t = 0; t < 10; t++) {
14 | setTimeout(() => {
15 |
16 | lastPoint = this.props.points[this.props.points.length - 1];
17 | if (lastPoint.y !== 1) {
18 | updateProps({ iter: this.props.iter + 1 });
19 | }
20 |
21 | }, 80 * t);
22 | }
23 | }
24 | }} />
25 | );
26 | }
27 | }
28 |
29 | export default multiRiffle;
--------------------------------------------------------------------------------
/components/positionChart.js:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 | const D3Component = require('idyll-d3-component');
3 | const d3 = require('d3');
4 | const annotation = require('d3-svg-annotation')
5 |
6 | const chartWidth = 650
7 | const chartHeight = 425
8 | const margin = ({ top: 110, right: 20, bottom: 50, left: 50 })
9 |
10 | const circleRadius = 3;
11 | const circleOpacity = 1;
12 |
13 | let xScale = d3.scaleLinear()
14 | .domain([0, 320])
15 | .range([margin.left, chartWidth - margin.right])
16 |
17 | let yScale = d3.scaleLinear()
18 | .domain([52, 1])
19 | .range([chartHeight - margin.bottom, margin.top])
20 |
21 | let lineData = [{ x: 236, y: -1 }, { x: 236, y: 52 }];
22 | let topLineData = [{ x: 0, y: 1 }, { x: 320, y: 1 }];
23 |
24 | const expectedLabelInitHeight = 75;
25 |
26 | const type = annotation.annotationCalloutElbow
27 |
28 | let annotations = [{
29 | note: {
30 | label: "The king is at the top when it reaches this line"
31 | },
32 | data: { iter: 140, position: 1 },
33 | dy: -40,
34 | dx: -130,
35 | connector: { end: "arrow" },
36 | color: '#aaaaaa'
37 | },
38 | {
39 | note: {
40 | label: "Expected number of riffles before the king reaches the top"
41 | },
42 | data: { iter: 236, position: 15 },
43 | dy: 90,
44 | dx: 25,
45 | connector: { end: "arrow" },
46 | color: '#aaaaaa'
47 | }]
48 |
49 | const makeAnnotations = annotation.annotation()
50 | // .editMode(true)
51 | .type(type)
52 | .accessors({
53 | x: d => xScale(d.iter),
54 | y: d => yScale(d.position)
55 | })
56 | .accessorsInverse({
57 | iter: d => xScale.invert(d.x),
58 | position: d => yScale.invert(d.y)
59 | })
60 | .annotations(annotations)
61 |
62 |
63 | class positionChart extends D3Component {
64 |
65 | initialize(node, props) {
66 |
67 | const svg = this.svg = d3.select(node).append('svg');
68 | svg.attr('viewBox', `0 0 ${chartWidth} ${chartHeight}`)
69 | .style('width', '100%')
70 | .style('height', 'auto');
71 | // .style('overflow', 'visible');
72 |
73 | let xAxis = d3.axisBottom(xScale)
74 | let yAxis = d3.axisLeft(yScale)
75 |
76 | svg.append('g')
77 | .attr('id', 'x-axis')
78 | .attr('transform', 'translate(0,' + (chartHeight - margin.bottom) + ')')
79 | .call(xAxis);
80 |
81 | svg.append('g')
82 | .attr('transform', 'translate(' + margin.left + ', 0)')
83 | .call(yAxis);
84 |
85 | svg.append("text")
86 | .attr("transform", "rotate(-90)")
87 | .attr("y", margin.left / 2)
88 | .attr("x", 0 - (chartHeight / 2)-30)
89 | .style("text-anchor", "middle")
90 | .text("position");
91 |
92 | svg.append("text")
93 | .attr("transform", "translate(" + (chartWidth / 2) + " ," + (chartHeight - 10) + ")")
94 | .style("text-anchor", "middle")
95 | .text("number of riffles");
96 |
97 | svg.append("g")
98 | .attr("stroke", "#000")
99 | .attr("stroke-opacity", 0.2)
100 | .selectAll("circle")
101 | .data(props.points)
102 | .enter().append("circle")
103 | .attr('class', 'point')
104 | .attr("cx", d => xScale(d.x))
105 | .attr("cy", d => yScale(d.y))
106 | .attr("r", circleRadius)
107 | .attr("opacity", circleOpacity);
108 |
109 | let annotationLineGenerator = d3.line().x(function (d) { return xScale(d.x) }).y(function (d) { return yScale(d.y) });
110 | let topLineGenerator = d3.line().x(function (d) { return xScale(d.x) }).y(function (d) { return yScale(d.y) });
111 |
112 | svg.append("g")
113 | .append('path')
114 | .attr('d', annotationLineGenerator(lineData))
115 | .style("stroke-dasharray", ("6, 6"))
116 | .attr('stroke', '#f44336')
117 | .attr('stroke-width', 3)
118 | .attr('fill', 'none')
119 | .attr('id', 'expected-riffle-count');
120 |
121 | svg.append("g")
122 | .append("text")
123 | .attr('id', 'expected-riffle-count-label')
124 | .attr("y", yScale(1) - 25)
125 | .attr("x", xScale(236) + 4)
126 | .style("text-anchor", "middle")
127 | .attr('transform', function (d) { return 'rotate(270,' + (xScale(236) + 4) + ',' + (yScale(1) - 25) + ')' })
128 | .text("236")
129 | .style('fill', '#f44336')
130 | .style('font-size', 12)
131 |
132 | svg.append("g")
133 | .append('text')
134 | .text('K♦')
135 | .attr('x', xScale(0) + 5)
136 | .attr('y', yScale(52) - 5)
137 | .attr('id', 'chart-annotation')
138 | .attr('fill', '#f44336')
139 | .style('font-weight', 700);
140 |
141 | svg.append("g")
142 | .append('path')
143 | .attr('d', topLineGenerator(topLineData))
144 | .attr('stroke', '#cccccc')
145 | .attr('stroke-width', 1)
146 | .attr('fill', 'none')
147 | .attr('id', 'top-position');
148 |
149 | svg.append("g")
150 | .attr("class", "annotation-group")
151 | .style('font-family', 'Playfair Display')
152 | .style('font-size', '14px')
153 | .call(makeAnnotations)
154 | }
155 |
156 | update(props) {
157 |
158 | let newestPoint = props.points[props.points.length - 1]
159 | xScale.domain([0, d3.max([320, newestPoint.x])]);
160 | d3.select('#x-axis').call(d3.axisBottom(xScale))
161 |
162 | if (props.iterVal === 0) {
163 | this.svg.selectAll('circle').remove();
164 | }
165 |
166 | this.svg.selectAll('circle')
167 | .data(props.points)
168 | .enter().append("circle")
169 | .attr('class', 'point')
170 | .attr("cx", d => xScale(d.x))
171 | .attr("cy", d => yScale(d.y))
172 | .attr("r", circleRadius)
173 | .attr("opacity", circleOpacity);
174 |
175 | this.svg.selectAll('circle')
176 | .data(props.points)
177 | .attr("cx", d => xScale(d.x))
178 | .attr("cy", d => yScale(d.y))
179 | .attr("r", circleRadius)
180 | .attr("opacity", circleOpacity)
181 | // .attr("fill", function (d) {
182 | // if (d.y === 1) {
183 | // return '#f44336'
184 | // }
185 | // })
186 | // .attr("r", function (d) {
187 | // if (d.y === 1) {
188 | // return 2*circleRadius;
189 | // } else {
190 | // return circleRadius;
191 | // }
192 | // });
193 |
194 | this.svg.select('#chart-annotation')
195 | .attr('x', xScale(newestPoint.x) + 5)
196 | .attr('y', yScale(newestPoint.y) - 5)
197 |
198 | let clearLineGenerator = d3.line().x(function (d) { return xScale(d.iter) }).y(function (d) { return yScale(d.position) });
199 |
200 | this.svg.selectAll('.endPoint')
201 | .data(props.endPoints)
202 | .enter()
203 | .append("g")
204 | .append('path')
205 | .attr('class', 'endPoint')
206 | .attr('d', function (d) {
207 | console.log(d);
208 | console.log(xScale(d.iter));
209 | console.log(yScale(d.position));
210 | return clearLineGenerator(d)
211 | })
212 | .attr('stroke', 'gray')
213 | .attr('stroke-width', 2)
214 | .attr('fill', 'none')
215 |
216 | this.svg.selectAll('.endPointLabel')
217 | .data(props.endPoints)
218 | .enter()
219 | .append("text")
220 | .attr('class', 'endPointLabel')
221 | .attr("y", function (d) { return yScale(1) - 25 })
222 | .attr("x", function (d) { return xScale(d[0].iter) + 4 })
223 | .style("text-anchor", "middle")
224 | .attr('transform', function (d) { return 'rotate(270,' + (xScale(d[0].iter) + 4) + ',' + (yScale(1) - 25) + ')' })
225 | .text(function (d) { return d[0].iter })
226 | .style('fill', 'gray')
227 | .style('font-size', 12)
228 |
229 | this.svg.selectAll('.endPoint')
230 | .data(props.endPoints)
231 | .attr('d', function (d) { return clearLineGenerator(d) })
232 |
233 | this.svg.selectAll('.endPointLabel')
234 | .data(props.endPoints)
235 | .attr("y", function (d) { return yScale(1) - 25 })
236 | .attr("x", function (d) { return xScale(d[0].iter) + 4 })
237 | .attr('transform', function (d) { return 'rotate(270,' + (xScale(d[0].iter) + 4) + ',' + (yScale(1) - 25) + ')' })
238 |
239 | let annotationLineGenerator = d3.line().x(function (d) { return xScale(d.x) }).y(function (d) { return yScale(d.y) });
240 |
241 | this.svg.selectAll('.expected-riffle-count-label')
242 | .attr("x", xScale(240))
243 |
244 | this.svg.select('#expected-riffle-count')
245 | .attr('d', annotationLineGenerator(lineData))
246 |
247 | this.svg.select('#expected-riffle-count-label')
248 | .attr("x", xScale(236) + 4)
249 | .attr('transform', function (d) { return 'rotate(270,' + (xScale(236) + 4) + ',' + (yScale(1) - 25) + ')' })
250 |
251 |
252 | }
253 | }
254 |
255 | module.exports = positionChart;
256 |
--------------------------------------------------------------------------------
/index.idyll:
--------------------------------------------------------------------------------
1 | [meta
2 | title:"The Math of Card Shuffling"
3 | description:"Riffling from factory order to complete randomness."
4 | shareImageUrl:"https://fredhohman.com/card-shuffling/static/images/share.png"
5 | shareImageWidth:"1600"
6 | shareImageHeight:"800" /]
7 |
8 | [Header
9 | title:"The Math of Card Shuffling"
10 | subtitle:"Riffling from factory order to complete randomness."
11 | author:"Fred Hohman"
12 | authorLink:"http://fredhohman.com" /]
13 |
14 | [Analytics google:"UA-42146340-1" /]
15 |
16 | You've probably seen a few ways to shuffle a deck of cards.
17 | Sometimes the deck is split in half and the halves are switched.
18 | Sometimes the deck is *[smooshed](https://big.assets.huffingtonpost.com/smooshing.gif)* until it's all mixed up.
19 | But most of the time, a deck of cards is shuffled using a *riffle*.
20 |
21 | // [aside]
22 | // Smooshing a deck is arguably more fun.
23 | // [image style:`{width: '100%'}` src:"static/images/smoosh.gif" /]
24 | // [/aside]
25 |
26 | [image style:`{width: '100%'}` src:"static/images/riffle.gif" /]
27 |
28 | Here's a question: *how many times do you have to riffle a deck of cards before it is completely shuffled?*
29 | // [br/]
30 | It's a tricky one, but math has us covered: [you need seven riffles](https://en.wikipedia.org/wiki/Shuffling#Riffle).
31 |
32 | [aside]
33 | We can calculate the number of orderings of a deck of cards using the notion of a [permutation](https://en.wikipedia.org/wiki/Permutation).
34 | To find all arrangements of 52 cards in a deck, we compute **52!**, which happens to be a [really big number](http://www.wolframalpha.com/input/?i=52!).
35 | [/aside]
36 |
37 | Riffle seven times and you'll have a sufficiently random ordering of cards, an ordering that has likely [never existed before](http://www.murderousmaths.co.uk/cardperms.htm).
38 | In other words, it's unlikely you'll ever shuffle two decks the same.
39 |
40 | The card shuffling result appears in a [Numberphile video from 2015](https://www.youtube.com/watch?v=AxJubaijQbI), along with a number of other card shuffling facts.
41 | Here's another problem posed in that video: what if instead of a standard riffle using a deck roughly split in half, you were to only riffle *1 card at a time*?
42 |
43 | [aside]
44 | [This paper](https://projecteuclid.org/download/pdf_1/euclid.aoap/1177005705) shows a number of other interesting card shuffling results in all their gory details.
45 | [/aside]
46 |
47 | That is, using a standard deck of 52 cards, in one hand hold 51 cards and in the other hold the remaining single card.
48 | Now riffle these together.
49 | This is equivalent to taking one card and placing it at random inside of the deck.
50 |
51 | **So here's the question:**
52 | [br/]
53 | *How many single riffles do you have to do in order to have a completely shuffled deck?*
54 |
55 | ## Theorem
56 | You could simulate this in a short program, which we will do towards the end, but first we can solve for the number of riffles explicitly.
57 |
58 | Consider an ordered deck of cards.
59 | [Without loss of generality](https://en.wikipedia.org/wiki/Without_loss_of_generality), let's say the suits are in the following order: [card suit:"S"/], [card suit:"C"/], [card suit:"H"/], [card suit:"D"/].
60 | So our ordered deck looks like this.
61 |
62 | [cardVis static:"True"/]
63 |
64 | The bottom suit is [card suit:"D"/], which means the bottom card of our deck is the King of Diamonds ([card number:"K" suit:"D"/]).
65 | Now perform the following iteration:
66 |
67 | >Place the top card of the deck randomly inside the deck
68 |
69 | This means taking the [card number:"A" suit:"S"/] and placing it randomly somewhere in the deck.
70 | The top card then becomes [card number:"2" suit:"S"/].
71 |
72 | If this procedure is repeated, eventually the top card will be placed at the very bottom of the deck, shifting the [card number:"K" suit:"D"/] to the penultimate position.
73 | Since every riffled card has a [Equation]\frac{1}{52}[/Equation] chance of moving to any new position in the deck, that means, on average, after about 52 top card riffles, the top card will become the new bottom card.
74 |
75 | [aside]
76 | **Note:** notice the [card number:"K" suit:"D"/] can only rise in the deck.
77 | There are two cases:
78 | 1. The top card is placed above the [card number:"K" suit:"D"/], therefore its position does not change.
79 | 2. The top card is placed underneath the [card number:"K" suit:"D"/], therefore it rises one position closer to the top.
80 | [/aside]
81 |
82 | Once the [card number:"K" suit:"D"/] moves up one position, upon subsequent riffles there are now two spots for the new top card to be placed underneath it.
83 | That means there is now a [Equation]\frac{1}{52}+\frac{1}{52}=\frac{2}{52}[/Equation] chance of a riffled card going underneath the [card number:"K" suit:"D"/].
84 |
85 | Continuing this procedure, the original bottom card, the [card number:"K" suit:"D"/], will eventually rise to the top of the deck and be riffled.
86 | Once this happens, the deck is randomly shuffled: the order we're left with is equally as likely as any other order.
87 |
88 | So, how many single card riffles does this take?
89 | Recall each time a card is placed underneath the [card number:"K" suit:"D"/], our chances of placing another card increases by [Equation]\frac{1}{52}[/Equation].
90 | We can calculate the number of riffles this would take.
91 |
92 | [Equation display:true]
93 | \sum_{i=1}^{52} \frac{52}{i} = \frac{52}{1} + \frac{52}{2} + \frac{52}{3} + ... + \frac{52}{52} \approx 236
94 | [/Equation]
95 |
96 | On average, 236 single card riffles will randomly shuffle a deck of cards.
97 |
98 | ## Let's Riffle
99 | Equations are great, but let's visualize this!
100 | Below is the same ordered deck of cards from before, except the [card number:"K" suit:"D"/] has been highlighted red so we can follow its journey to the top of the deck.
101 |
102 | Click the **Riffle** button to move the top card somewhere else in the deck randomly.
103 |
104 | Did you see where it went? Click again.
105 |
106 | Click a bunch more really fast.
107 |
108 | Now I could tell you to keep clicking until the highlighted [card number:"K" suit:"D"/] rises to the top, but as we have already shown, that would take about 236 clicks.
109 | Instead, click the **Riffle (x10)** button to riffle 10 times.
110 | Keep riffling until the [card number:"K" suit:"D"/] moves to the top.
111 |
112 | [var name:"iter" value:0 /]
113 | [var name:"points" value:`[{x:0, y:52}]` /]
114 | [var name:"endPoints" value:`[]` /]
115 |
116 | [aside]
117 | You have riffled **[Display value:iter format:"d" /]** times.
118 | [br/]
119 | [button onClick:`if(points[points.length-1].y !== 1){iter++}`]Riffle[/button]
120 | [multiRiffle iter:iter points:points ]Riffle (x10)[/multiRiffle]
121 | [/aside]
122 |
123 | [cardVis iterVar:iter points:points /]
124 |
125 | Here is a chart of the [card number:"K" suit:"D"/]'s position in the deck for each riffle.
126 | Notice how it takes many riffles to move the [card number:"K" suit:"D"/] up just a few positions, but once the [card number:"K" suit:"D"/] starts rising towards the top of the deck, it accelerates.
127 |
128 | [aside]
129 | Once the [card number:"K" suit:"D"/] is the top card, click the **Clear** button to try again.
130 | [button className:"clear" onClick:`endPoints.push([{iter:iter, position:-1}, {iter:iter, position:3}]); iter=-1; points=[{x:0, y:52}]; iter++;`]Clear[/button]
131 | [/aside]
132 | [positionChart iterVal:iter points:points endPoints:endPoints /]
133 |
134 | On average, the [card number:"K" suit:"D"/] will reach the top position somewhere around 236 riffles.
135 | Since this is the *average* result, there is a chance your first shuffled deck of cards took less riffles (or many more!).
136 | To try again, click the **Clear** button and get riffling.
137 |
138 | // We can simulate this multiple times and verify that on average it takes 236 times.
139 |
140 | ### Acknowledgements
141 | * This article was created using [a href:"https://idyll-lang.org/"]Idyll[/a].
142 | * Shoutout to [a href:"https://twitter.com/mathisonian"]@mathisonian[/a] for help and feedback.
143 | * The source code is available on [a href:"https://github.com/fredhohman/card-shuffling/"]Github[/a].
144 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "card-shuffling",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "start": "idyll index.idyll --defaultComponents=components/default --theme idyll --css styles.css --template _index.html --watch",
7 | "build": "idyll index.idyll --defaultComponents=components/default --theme idyll --css styles.css --template _index.html",
8 | "deploy": "npm run build && gh-pages -d ./build"
9 | },
10 | "dependencies": {
11 | "d3": "^4.0.0",
12 | "d3-svg-annotation": "^2.2.5",
13 | "idyll": "^2.0.0",
14 | "idyll-d3-component": "^2.0.0"
15 | },
16 | "devDependencies": {
17 | "gh-pages": "^0.12.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/static/images/riffle.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredhohman/card-shuffling/2c66f30d3bf7f3ef6b6dc28ae39de6322e0de0c1/static/images/riffle.gif
--------------------------------------------------------------------------------
/static/images/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredhohman/card-shuffling/2c66f30d3bf7f3ef6b6dc28ae39de6322e0de0c1/static/images/share.png
--------------------------------------------------------------------------------
/static/images/smoosh.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fredhohman/card-shuffling/2c66f30d3bf7f3ef6b6dc28ae39de6322e0de0c1/static/images/smoosh.gif
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | /* Put your custom styles here */
2 |
3 | /*@import url('https://fonts.googleapis.com/css?family=Open+Sans');*/
4 |
5 | body {
6 | font-family: 'Open Sans', sans-serif;
7 | }
8 |
9 | a, a:visited, a:hover {
10 | color: black;
11 | cursor: pointer;
12 | text-decoration: none;
13 | box-shadow: inset 0 -3px 0 #f44336;
14 | transition: box-shadow 0.25s ease-out, color 0.25s ease-out;
15 | }
16 |
17 | a:hover {
18 | color: white;
19 | box-shadow: inset 0 -20px 0 #f44336;
20 | }
21 |
22 | .card:hover {
23 | /* fill: #eeeeee; */
24 | }
25 |
26 | .hed {
27 | font-size: 4rem;
28 | font-family: 'Playfair Display';
29 | /* font-weight: 300; */
30 | /* font-family: sans-serif; */
31 | /* text-transform: uppercase; */
32 | width: 85%;
33 | line-height: 4.5rem;
34 | }
35 |
36 | .dek {
37 | font-weight: 300;
38 | /* text-transform: uppercase; */
39 | }
40 |
41 | .byline {
42 | /* font-weight: 300; */
43 | /* text-transform: uppercase; */
44 | }
45 |
46 | button {
47 | cursor: pointer;
48 | border: 1px solid black;
49 | background: none;
50 | box-shadow: none;
51 | border-radius: 0px;
52 | padding: 10px 20px 10px 20px;
53 | margin: 5px;
54 | font-size: 14px;
55 | font-weight: 700;
56 | }
57 |
58 | button:hover {
59 | background-color: #eeeeee;
60 | }
61 |
62 | button:active {
63 | background-color: #dddddd;
64 | }
65 |
66 | button:focus {
67 | outline: 0;
68 | }
69 |
70 | .clear {
71 | background-color: #444444;
72 | color: white;
73 | border: 1px solid white;
74 | }
75 |
76 | .clear:hover {
77 | background-color: #666666;
78 | }
79 |
80 | blockquote {
81 | font-style: italic;
82 | padding: 18px;
83 | font-size: 1.15em;
84 | border-left: 5px solid black;
85 | }
86 |
87 | @media all and (max-width: 1000px) {
88 | .idyll-text-container {
89 | margin-left: 0 !important;
90 | }
91 | }
--------------------------------------------------------------------------------