-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
200 lines (161 loc) · 5.76 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<!DOCTYPE html>
<meta name="robots" content="noindex">
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<meta name=Description content="Gaussian distribution...">
<meta name=keywords content="joke, animation, Gauss">
<meta name=author content="Andrei Kashcha">
<title>Gaussian Distribution</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/1.3.5/chroma.min.js"></script>
</head>
<body>
<script id="jsbin-javascript">
var colorSet = new Map(); // maps color value to pixel info
var img = document.createElement('img');
var canvas = document.createElement('canvas');
var ctx;
// I use state to determine whether it should expand or collapse
var currentStage = 1;
// This is to properly scale the height of the chart
var maxColorsInBin = 0;
// Width and height of the working image
var width, height;
// Make sure image has CORS setup, otherwise we will not have access
// to it
img.crossOrigin = 'Anonymous';
// When image is loaded - run the code.
img.onload = function() {
width = canvas.width = img.width;
height = canvas.height = img.height;
start();
}
// Trigger image load. The image was originally taken from
// https://en.wikipedia.org/wiki/Carl_Friedrich_Gauss
img.src = 'https://i.imgur.com/1JBBNXY.jpg'// 'https://i.imgur.com/iyf2bRA.png';
// https://cdna.artstation.com/p/assets/images/images/002/955/808/large/nik-hagialas-cat-painting-for-computer.jpg
function start() {
ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
width = canvas.width;
height = canvas.height;
var imgData = ctx.getImageData(0, 0, width, height);
var pixels = imgData.data;
// We are going to collect pixel values first
for (var x = 0; x < width; ++x) {
for (var y = 0; y < height; ++y) {
var i = (x + y * width) * 4;
var r = pixels[i + 0];
var g = pixels[i + 1];
var b = pixels[i + 2];
var a = pixels[i + 3];
var color = chroma(r, g, b)
var colorKey = color.get('hsl.l')
var pixelInfo = colorSet.get(colorKey);
if (!pixelInfo) {
pixelInfo = {
points: [],
};
colorSet.set(colorKey, pixelInfo);
}
pixelInfo.points.push({
// original position of a pixel
x: x,
y: y,
// Randomly assign animation lifespan
timeSpan: Math.round(Math.random() * 120) + 30,
// current frame number, used for interpolation
frame: 0,
// Where this pixel should go? Will be computed by computeDestinations()
destX: 0,
destY: 0,
// Current color
color: color
});
// We need to know the highest point of a chart, to properly scale it inside
// available canvas
if (pixelInfo.points.length > maxColorsInBin) maxColorsInBin = pixelInfo.points.length;
}
}
// Now that we have all pixels collected, lets figure out.
computeDestinations();
document.body.appendChild(canvas);
// I did a small pause before initial animation, to set the stage
setTimeout(() => { requestAnimationFrame(move); }, 1000);
}
function interploate(t) {
// This is easeInOutQuad function. See more here https://gist.github.com/gre/1650294
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
function lerp(a, b, t) {
// simple linear interploation.
return b * t + a * (1 - t);
}
// Main animation loop
function move() {
// When all pixels cannot be moved anymore, we switch the mode.
var hasMore = false;
// Need to pick neutral background color to not blend with original colors
ctx.fillStyle = '#676767';
ctx.fillRect(0, 0, width, height);
var imgData = ctx.getImageData(0, 0, width, height);
var pixels = imgData.data;
// Okay, let's move the pixels
colorSet.forEach((pixelInfo, colorKey) => {
pixelInfo.points.forEach((point) => {
// given current frame, use easing to figure out offset of a pixel
var t = interploate(point.frame/point.timeSpan);
if (currentStage === 1 && point.frame < point.timeSpan) {
point.frame += 1;
// if at least one point was moved - keep trying on the next frame
hasMore = true;
} else if (currentStage === 2 & point.frame > 0) {
point.frame -= 1;
hasMore = true;
// I want to restore portrait faster than collapsing it:
if (point.frame > 0) point.frame -= 1;
}
var x = Math.round(lerp(point.x, point.destX, t));
var y = Math.round(lerp(point.y, point.destY, t));
// Color has four components, thus multplying by 4
var pixelIndex = (x + y * width) * 4;
var color = point.color.rgba();
pixels[pixelIndex + 0] = color[0];
pixels[pixelIndex + 1] = color[1];
pixels[pixelIndex + 2] = color[2];
pixels[pixelIndex + 3] = color[3] * 255;
});
});
// All pixels updated on this frame, lets draw them
ctx.putImageData(imgData, 0, 0);
if (!hasMore) {
// 3 is just a trick. If current state can be only 1 or 2, then
// assigning 3 - currentState guarantees that we flip only between
// these two states.
currentStage = 3 - currentStage;
hasMore = true;
// Start next loop after a little pause
setTimeout(() => requestAnimationFrame(move), 1000);
} else {
requestAnimationFrame(move);
}
}
function computeDestinations() {
// This function computes where the pixel should go, based on its value
var binCount = 256;
var binWidth = (width/binCount);
colorSet.forEach((pixelInfo, colorKey) => {
var pointBin = colorKey * binCount;
var xOffset = pointBin * binWidth;
var yOffset = 1/maxColorsInBin;
pixelInfo.points.forEach((point, idx) => {
point.destX = xOffset;
// I add small coefficients to make a padding.
point.destY = height * 0.85 - 0.80 * height * idx /maxColorsInBin;
})
});
}
</script>
</body>
</html>