happo 2.7.7 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/bin/happo +1 -1
  3. data/lib/happo/public/0144a92667ed2e8a8b13.worker.js +56 -0
  4. data/lib/happo/public/0622a1eb2a5b4dab0d7d.worker.js +62 -0
  5. data/lib/happo/public/088cc9d4615d51d37661.worker.js +68 -0
  6. data/lib/happo/public/11b439cedd6aa6a01e71.worker.js +68 -0
  7. data/lib/happo/public/15b1495833b5b13f5f51.worker.js +62 -0
  8. data/lib/happo/public/167f0a5b59a8c27815fe.worker.js +62 -0
  9. data/lib/happo/public/19f5f7403fb4a2ba42e2.worker.js +62 -0
  10. data/lib/happo/public/1a86029ce53b1aa81f68.worker.js +62 -0
  11. data/lib/happo/public/1b6803fbc7b07f61cba2.worker.js +74 -0
  12. data/lib/happo/public/1d3dc560f1f39393dafe.worker.js +62 -0
  13. data/lib/happo/public/203272bf00c349325c60.worker.js +62 -0
  14. data/lib/happo/public/220e711d4abdbc4bd32f.worker.js +62 -0
  15. data/lib/happo/public/25ad7a1e52bd424ca81d.worker.js +62 -0
  16. data/lib/happo/public/26d95a76207ba0fe6c25.worker.js +74 -0
  17. data/lib/happo/public/28237f0668f12dfb456c.worker.js +62 -0
  18. data/lib/happo/public/2a7a935f3b9526c6f7ab.worker.js +74 -0
  19. data/lib/happo/public/2c00cb8d0a2eb5dd4078.worker.js +68 -0
  20. data/lib/happo/public/2c71ddb5b15a755d4644.worker.js +68 -0
  21. data/lib/happo/public/32648d3dfb22995f4066.worker.js +68 -0
  22. data/lib/happo/public/3452650686a8096b2bb7.worker.js +68 -0
  23. data/lib/happo/public/346cefcc1572890455b8.worker.js +68 -0
  24. data/lib/happo/public/35bd63f58bb69ac267ea.worker.js +68 -0
  25. data/lib/happo/public/360e0acb872760f43b07.worker.js +68 -0
  26. data/lib/happo/public/3818f1c91ce4d4d8b724.worker.js +62 -0
  27. data/lib/happo/public/3cc2e649d9b6ff62f4ab.worker.js +62 -0
  28. data/lib/happo/public/3deccedc8fbb96bb1aa7.worker.js +68 -0
  29. data/lib/happo/public/3e672be26a7a8864a342.worker.js +68 -0
  30. data/lib/happo/public/41aa7471344a79ff98f2.worker.js +68 -0
  31. data/lib/happo/public/423ba9f549f3829873e6.worker.js +62 -0
  32. data/lib/happo/public/463eb0ecadbb11365ca4.worker.js +62 -0
  33. data/lib/happo/public/47c52ff0e060fedb841f.worker.js +62 -0
  34. data/lib/happo/public/4e685162de1d8a9b102d.worker.js +56 -0
  35. data/lib/happo/public/4eea1b4008ed68e41f82.worker.js +62 -0
  36. data/lib/happo/public/4f6b0faf1b43e93a1404.worker.js +62 -0
  37. data/lib/happo/public/54ba8bc7a595358209d2.worker.js +68 -0
  38. data/lib/happo/public/589c2f9547d240ac8a57.worker.js +68 -0
  39. data/lib/happo/public/5a8c0588ca745d7904f7.worker.js +68 -0
  40. data/lib/happo/public/5b3cabd81f7d8688f7a5.worker.js +62 -0
  41. data/lib/happo/public/5f5c9f07cb5117c523a2.worker.js +68 -0
  42. data/lib/happo/public/5fb8c7066659ea1b57e2.worker.js +62 -0
  43. data/lib/happo/public/5fb962cc191a60b42af0.worker.js +68 -0
  44. data/lib/happo/public/62c7585d1d23297b316f.worker.js +62 -0
  45. data/lib/happo/public/62e676d31cbf8aaa9359.worker.js +68 -0
  46. data/lib/happo/public/65efcf6aee5e3ef33539.worker.js +68 -0
  47. data/lib/happo/public/693d4918a5dae465c134.worker.js +68 -0
  48. data/lib/happo/public/69d106b071dd31ad86de.worker.js +68 -0
  49. data/lib/happo/public/730c035f5c2b404ff225.worker.js +62 -0
  50. data/lib/happo/public/7609088c49b73ee2e3e1.worker.js +68 -0
  51. data/lib/happo/public/76161d401db5bb36980a.worker.js +62 -0
  52. data/lib/happo/public/7d9febb46b37ddffa2d5.worker.js +62 -0
  53. data/lib/happo/public/7f29af56bd0ea82d77d9.worker.js +68 -0
  54. data/lib/happo/public/80009e8bc0ffd29d3390.worker.js +62 -0
  55. data/lib/happo/public/830db6cc9c99c2e8e9c1.worker.js +62 -0
  56. data/lib/happo/public/85ad69599657de02d20d.worker.js +68 -0
  57. data/lib/happo/public/875e790f3e64476b3aa0.worker.js +68 -0
  58. data/lib/happo/public/8fa9a7b2a5f19075eedc.worker.js +74 -0
  59. data/lib/happo/public/906ce908877052838b8e.worker.js +62 -0
  60. data/lib/happo/public/9472102d9a6d2577f988.worker.js +62 -0
  61. data/lib/happo/public/9ac309ab34f48e7af07c.worker.js +68 -0
  62. data/lib/happo/public/9cdd6f0763fd866a5118.worker.js +62 -0
  63. data/lib/happo/public/HappoApp.bundle.js +2 -0
  64. data/lib/happo/public/a8bfebcafc4935d15b00.worker.js +1 -0
  65. data/lib/happo/public/aa7ae8013f9780eb8acb.worker.js +68 -0
  66. data/lib/happo/public/b20a32590dfe259f4fe3.worker.js +62 -0
  67. data/lib/happo/public/b25ebdd4e2d2dee8c7b1.worker.js +68 -0
  68. data/lib/happo/public/b7a49d603b1d50f535b2.worker.js +68 -0
  69. data/lib/happo/public/b8e667564ad96f5c77f4.worker.js +68 -0
  70. data/lib/happo/public/b913c562805ec9a01635.worker.js +68 -0
  71. data/lib/happo/public/bab832c7110d15a5f43b.worker.js +62 -0
  72. data/lib/happo/public/bfcf99c9b2bf3d58ec7f.worker.js +62 -0
  73. data/lib/happo/public/c0aeff8b32c9911c8547.worker.js +68 -0
  74. data/lib/happo/public/c2297ce40f0d071ec173.worker.js +1 -0
  75. data/lib/happo/public/c3d7b17359c21ff82281.worker.js +68 -0
  76. data/lib/happo/public/c9f7adcce3de80eb759a.worker.js +68 -0
  77. data/lib/happo/public/card-current.png +0 -0
  78. data/lib/happo/public/card-previous.png +0 -0
  79. data/lib/happo/public/d7b87aec5f1e4f2a7a5d.worker.js +68 -0
  80. data/lib/happo/public/d8fefa1bd98baa4e38da.worker.js +62 -0
  81. data/lib/happo/public/db617ae6458966f4acae.worker.js +68 -0
  82. data/lib/happo/public/dialog-current.png +0 -0
  83. data/lib/happo/public/dialog-previous.png +0 -0
  84. data/lib/happo/public/e247eaa7506f44b5b847.worker.js +62 -0
  85. data/lib/happo/public/e92bb4e1e06843fa771e.worker.js +62 -0
  86. data/lib/happo/public/e9e2d97848e0059e15c5.worker.js +62 -0
  87. data/lib/happo/public/ea87c48552c960ed7a1c.worker.js +74 -0
  88. data/lib/happo/public/eb5aafba8d16a397517a.worker.js +68 -0
  89. data/lib/happo/public/edc218b655b9ef694b3e.worker.js +74 -0
  90. data/lib/happo/public/eea64a6efa0620a576d7.worker.js +68 -0
  91. data/lib/happo/public/f269a8e7ffca13e9366a.worker.js +68 -0
  92. data/lib/happo/public/f81ed0eec7edfa2dfadb.worker.js +62 -0
  93. data/lib/happo/public/fcd20ee3e5a10e8667bb.worker.js +68 -0
  94. data/lib/happo/public/full-page-large-current.png +0 -0
  95. data/lib/happo/public/full-page-large-previous.png +0 -0
  96. data/lib/happo/public/full-page-small-current.png +0 -0
  97. data/lib/happo/public/full-page-small-previous.png +0 -0
  98. data/lib/happo/public/happo-styles.css +28 -3
  99. data/lib/happo/public/major-diff-large-current.png +0 -0
  100. data/lib/happo/public/major-diff-large-previous.png +0 -0
  101. data/lib/happo/public/major-diff-small-current.png +0 -0
  102. data/lib/happo/public/major-diff-small-previous.png +0 -0
  103. data/lib/happo/public/modal-current.png +0 -0
  104. data/lib/happo/public/modal-previous.png +0 -0
  105. data/lib/happo/public/small-current.png +0 -0
  106. data/lib/happo/public/small-previous.png +0 -0
  107. data/lib/happo/public/wide-current.png +0 -0
  108. data/lib/happo/public/wide-previous.png +0 -0
  109. data/lib/happo/runner.rb +25 -31
  110. data/lib/happo/server.rb +81 -12
  111. data/lib/happo/uploader.rb +1 -2
  112. data/lib/happo/utils.rb +1 -1
  113. data/lib/happo/version.rb +1 -1
  114. data/lib/happo/views/diffs.erb +9 -14
  115. data/lib/happo.rb +0 -1
  116. metadata +107 -21
  117. data/lib/happo/public/HappoDiffs.jsx +0 -391
  118. data/lib/happo/snapshot_comparer.rb +0 -81
  119. data/lib/happo/snapshot_comparison_image/base.rb +0 -108
  120. data/lib/happo/snapshot_comparison_image/gutter.rb +0 -34
  121. data/lib/happo/snapshot_comparison_image/overlayed.rb +0 -88
@@ -1,391 +0,0 @@
1
- /* global React */
2
- /* eslint-disable react/no-multi-comp */
3
- const PropTypes = React.PropTypes;
4
-
5
- const VIEWS = {
6
- SIDE_BY_SIDE: 'Side-by-side',
7
- SWIPE: 'Swipe',
8
- DIFF: 'Diff',
9
- };
10
-
11
- const imageShape = {
12
- description: PropTypes.string.isRequired,
13
- viewport: PropTypes.string.isRequired,
14
- diff: PropTypes.string,
15
- previous: PropTypes.string,
16
- current: PropTypes.string.isRequired,
17
- };
18
-
19
- function imageSlug(image) {
20
- return btoa(image.description + image.viewport);
21
- }
22
-
23
- function InlineLink({ children, to }) {
24
- return (
25
- <a className='InlineLink' href={`#${to}`}>
26
- {children}
27
- </a>
28
- );
29
- }
30
- InlineLink.propTypes = {
31
- children: PropTypes.node.isRequired,
32
- to: PropTypes.string.isRequired,
33
- };
34
-
35
- function ImageHeading({ image }) {
36
- return (
37
- <h3 id={imageSlug(image)}>
38
- <InlineLink to={imageSlug(image)}>
39
- {image.description}
40
- {' @ '}
41
- {image.viewport}
42
- </InlineLink>
43
- </h3>
44
- );
45
- }
46
- ImageHeading.propTypes = {
47
- image: PropTypes.shape(imageShape).isRequired,
48
- };
49
-
50
- function NewImage({ image }) {
51
- return (
52
- <div>
53
- <ImageHeading
54
- image={image}
55
- />
56
- <img
57
- role='presentation'
58
- src={image.current}
59
- />
60
- </div>
61
- );
62
- }
63
- NewImage.propTypes = {
64
- image: PropTypes.shape(imageShape).isRequired,
65
- };
66
-
67
- function DiffImages({ images }) {
68
- if (!images.length) {
69
- return null;
70
- }
71
-
72
- return (
73
- <div>
74
- <h2 id='diffs'>
75
- <InlineLink to='diffs'>
76
- Diffs ({ images.length })
77
- </InlineLink>
78
- </h2>
79
-
80
- {images.map((image) => (
81
- <DiffController
82
- key={image.current}
83
- image={image}
84
- />
85
- ))}
86
- </div>
87
- );
88
- }
89
- DiffImages.propTypes = {
90
- images: PropTypes.arrayOf(PropTypes.shape(imageShape)).isRequired,
91
- };
92
-
93
- function NewImages({ images }) {
94
- if (!images.length) {
95
- return null;
96
- }
97
-
98
- return (
99
- <div>
100
- <h2 id='new'>
101
- <InlineLink to='new'>
102
- New examples ({ images.length })
103
- </InlineLink>
104
- </h2>
105
-
106
- {images.map((image) => (
107
- <NewImage
108
- key={image.current}
109
- image={image}
110
- />
111
- ))}
112
- </div>
113
- );
114
- }
115
- NewImages.propTypes = {
116
- images: PropTypes.arrayOf(PropTypes.shape(imageShape)).isRequired,
117
- };
118
-
119
- function maxImageSize(...imageUrls) {
120
- const dimensions = {};
121
-
122
- return new Promise((resolve, reject) => {
123
- imageUrls.forEach((url, i) => {
124
- const image = new Image();
125
-
126
- image.onerrer = function handleImageError(e) {
127
- reject(e);
128
- };
129
-
130
- image.onload = function handleImageLoad() {
131
- const { width, height } = this;
132
-
133
- // Use the index in case the URL is somehow duplicated.
134
- dimensions[i] = { width, height };
135
-
136
- if (Object.keys(dimensions).length >= imageUrls.length) {
137
- // We are done, so compute the max width and height and resolve.
138
- const values = Object.keys(dimensions).map(key => dimensions[key]);
139
- const maxWidth = Math.max(...values.map(value => value.width));
140
- const maxHeight = Math.max(...values.map(value => value.height));
141
- resolve({ width: maxWidth, height: maxHeight });
142
- }
143
- };
144
-
145
- image.src = url;
146
- });
147
- });
148
- }
149
-
150
- class Swiper extends React.Component {
151
- constructor(props) {
152
- super(props);
153
- this.state = {
154
- cursorLeft: 0,
155
- height: 'auto',
156
- width: 'auto',
157
- };
158
- this.handleMouseMove = this.handleMouseMove.bind(this);
159
- }
160
-
161
- componentWillMount() {
162
- this.updateSize(this.props)
163
- .then(({ width }) => {
164
- // Start in the center
165
- this.setState({ cursorLeft: width / 2 });
166
- });
167
- }
168
-
169
- componentWillReceiveProps(nextProps) {
170
- this.updateSize(nextProps);
171
- }
172
-
173
- updateSize({ current, previous }) {
174
- const sizes = maxImageSize(current, previous)
175
- .then(({ width, height }) => {
176
- this.setState({ width, height });
177
- return { width, height };
178
- });
179
-
180
- return Promise.resolve(sizes);
181
- }
182
-
183
- handleMouseMove(event) {
184
- this.setState({
185
- cursorLeft: event.pageX - event.target.offsetLeft,
186
- });
187
- }
188
-
189
- render() {
190
- const { previous, current } = this.props;
191
- const { cursorLeft, height, width } = this.state;
192
-
193
- return (
194
- <div
195
- className='Swiper'
196
- style={{ height, width }}
197
- onMouseMove={this.handleMouseMove}
198
- >
199
- <div
200
- className='Swiper__image'
201
- style={{ width: cursorLeft }}
202
- >
203
- <img
204
- src={previous}
205
- role='presentation'
206
- />
207
- </div>
208
-
209
- <div
210
- className='Swiper__image'
211
- style={{
212
- transform: `translateX(${cursorLeft}px)`,
213
- width: width - cursorLeft,
214
- }}
215
- >
216
- <img
217
- src={current}
218
- style={{
219
- transform: `translateX(-${cursorLeft}px)`,
220
- }}
221
- role='presentation'
222
- />
223
- </div>
224
-
225
- <div
226
- className='Swiper__cursor'
227
- style={{
228
- transform: `translateX(${cursorLeft}px)`,
229
- }}
230
- />
231
- </div>
232
- );
233
- }
234
- }
235
- Swiper.propTypes = {
236
- previous: PropTypes.string.isRequired,
237
- current: PropTypes.string.isRequired,
238
- };
239
-
240
- function SideBySide({ previous, current }) {
241
- return (
242
- <div className='SideBySide'>
243
- <img
244
- className='SideBySide__image'
245
- role='presentation'
246
- src={previous}
247
- title='Before'
248
- />
249
- {' '}
250
- <img
251
- className='SideBySide__image'
252
- role='presentation'
253
- src={current}
254
- title='After'
255
- />
256
- </div>
257
- );
258
- }
259
- SideBySide.propTypes = {
260
- previous: PropTypes.string.isRequired,
261
- current: PropTypes.string.isRequired,
262
- };
263
-
264
- function SelectedView({ image, selectedView }) {
265
- if (selectedView === VIEWS.SIDE_BY_SIDE) {
266
- return (
267
- <SideBySide
268
- previous={image.previous}
269
- current={image.current}
270
- />
271
- );
272
- }
273
-
274
- if (selectedView === VIEWS.DIFF) {
275
- return (
276
- <img role='presentation' src={image.diff} />
277
- );
278
- }
279
-
280
- if (selectedView === VIEWS.SWIPE) {
281
- return (
282
- <Swiper
283
- previous={image.previous}
284
- current={image.current}
285
- />
286
- );
287
- }
288
- }
289
- SelectedView.propTypes = {
290
- image: PropTypes.shape(imageShape).isRequired,
291
- selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
292
- };
293
-
294
- class DiffController extends React.Component {
295
- constructor(props) {
296
- super(props);
297
- this.state = {
298
- selectedView: VIEWS.SIDE_BY_SIDE,
299
- };
300
-
301
- this.handleClick = this.handleClick.bind(this);
302
- }
303
-
304
- handleClick(view) {
305
- this.setState({ selectedView: view });
306
- }
307
-
308
- render() {
309
- return (
310
- <Diff
311
- image={this.props.image}
312
- selectedView={this.state.selectedView}
313
- onClick={this.handleClick}
314
- />
315
- );
316
- }
317
- }
318
- DiffController.propTypes = {
319
- image: PropTypes.shape(imageShape).isRequired,
320
- };
321
-
322
- function Diff({ image, selectedView, onClick }) {
323
- return (
324
- <div>
325
- <ImageHeading
326
- image={image}
327
- />
328
- <div className='Diff__buttons'>
329
- {Object.keys(VIEWS).map(key => VIEWS[key]).map((view, i) => {
330
- const classes = ['Diff__button'];
331
- if (i === 0) {
332
- classes.push('Diff__button--first');
333
- } else if (i === Object.keys(VIEWS).length - 1) {
334
- classes.push('Diff__button--last');
335
- }
336
-
337
- return (
338
- <button
339
- key={view}
340
- className={classes.join(' ')}
341
- aria-pressed={view === selectedView}
342
- onClick={() => { onClick(view); }}
343
- >
344
- {view}
345
- </button>
346
- );
347
- })}
348
- </div>
349
- <div className='Diff__images'>
350
- <SelectedView image={image} selectedView={selectedView} />
351
- </div>
352
- </div>
353
- );
354
- }
355
- Diff.propTypes = {
356
- image: PropTypes.shape(imageShape).isRequired,
357
- onClick: PropTypes.func.isRequired,
358
- selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
359
- };
360
-
361
- function HappoDiffs({ pageTitle, generatedAt, diffImages, newImages }) {
362
- return (
363
- <div>
364
- <header className='HappoDiffs__header'>
365
- <h1 className='HappoDiffs__headerTitle'>
366
- {pageTitle}
367
- </h1>
368
- <div className='HappoDiffs__headerTime'>
369
- Generated: {generatedAt}
370
- </div>
371
- </header>
372
-
373
- <main className='HappoDiffs__main'>
374
- <DiffImages
375
- images={diffImages}
376
- />
377
- <NewImages
378
- images={newImages}
379
- />
380
- </main>
381
- </div>
382
- );
383
- }
384
- HappoDiffs.propTypes = {
385
- pageTitle: PropTypes.string.isRequired,
386
- diffImages: PropTypes.arrayOf(imageShape).isRequired,
387
- newImages: PropTypes.arrayOf(imageShape).isRequired,
388
- generatedAt: PropTypes.string.isRequired,
389
- };
390
-
391
- window.HappoDiffs = HappoDiffs;
@@ -1,81 +0,0 @@
1
- require 'oily_png'
2
- require 'diff-lcs'
3
- require_relative 'snapshot_comparison_image/base'
4
- require_relative 'snapshot_comparison_image/gutter'
5
- require_relative 'snapshot_comparison_image/overlayed'
6
-
7
- module Happo
8
- # This class is responsible for comparing two Snapshots and generating a diff.
9
- class SnapshotComparer
10
- # @param png_before [ChunkyPNG::Image]
11
- # @param png_after [ChunkyPNG::Image]
12
- def initialize(png_before, png_after)
13
- @png_after = png_after
14
- @png_before = png_before
15
- end
16
-
17
- # @return [Hash]
18
- def compare!
19
- no_diff = {
20
- diff_in_percent: 0,
21
- diff_image: nil,
22
- }
23
-
24
- # If these images are totally identical, we don't need to do any more
25
- # work.
26
- return no_diff if @png_before == @png_after
27
-
28
- array_before = to_array_of_arrays(@png_before)
29
- array_after = to_array_of_arrays(@png_after)
30
-
31
- # If the arrays of arrays of colors are identical, we don't need to do any
32
- # more work. This might happen if some of the headers are different.
33
- return no_diff if array_before == array_after
34
-
35
- sdiff = Diff::LCS.sdiff(array_before, array_after)
36
- number_of_different_rows = 0
37
-
38
- sprite, all_comparisons = initialize_comparison_images(
39
- [@png_after.width, @png_before.width].max, sdiff.size
40
- )
41
-
42
- sdiff.each_with_index do |row, y|
43
- # each row is a Diff::LCS::ContextChange instance
44
- all_comparisons.each { |image| image.render_row(y, row) }
45
- number_of_different_rows += 1 unless row.unchanged?
46
- end
47
-
48
- percent_changed = number_of_different_rows.to_f / sdiff.size * 100
49
- {
50
- diff_in_percent: percent_changed,
51
- diff_image: (sprite if percent_changed > 0.0),
52
- }
53
- end
54
-
55
- private
56
-
57
- # @param [ChunkyPNG::Image]
58
- # @return [Array<Array<Integer>>]
59
- def to_array_of_arrays(chunky_png)
60
- array_of_arrays = []
61
- chunky_png.height.times do |y|
62
- array_of_arrays << chunky_png.row(y)
63
- end
64
- array_of_arrays
65
- end
66
-
67
- # @param canvas [ChunkyPNG::Image] The output image to draw pixels on
68
- # @return [Array<SnapshotComparisonImage>]
69
- def initialize_comparison_images(width, height)
70
- gutter_width = Happo::SnapshotComparisonImage::Gutter::WIDTH
71
- total_width = gutter_width + width
72
-
73
- sprite = ChunkyPNG::Image.new(total_width, height)
74
- offset, comparison_images = 0, []
75
- comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
76
- offset += gutter_width
77
- comparison_images << Happo::SnapshotComparisonImage::Overlayed.new(offset, sprite)
78
- [sprite, comparison_images]
79
- end
80
- end
81
- end
@@ -1,108 +0,0 @@
1
- require 'chunky_png'
2
-
3
- module Happo
4
- module SnapshotComparisonImage
5
- # This model represents a "comparison image". Basically it's just a wrapper
6
- # around a ChunkyPNG image with some nice methods to make life easier in the
7
- # world of diffs.
8
- #
9
- # This model is never persisted.
10
- class Base
11
- include ChunkyPNG::Color
12
-
13
- BASE_OPACITY = 0.1
14
- BASE_ALPHA = (255 * BASE_OPACITY).round
15
- BASE_DIFF_ALPHA = BASE_ALPHA * 2
16
-
17
- MAGENTA = ChunkyPNG::Color.from_hex '#b33682'
18
- RED = ChunkyPNG::Color.from_hex '#dc322f'
19
- GREEN = ChunkyPNG::Color.from_hex '#859900'
20
-
21
- # @param offset [Integer] the x-offset that this comparison image should
22
- # use when rendering on the canvas image.
23
- # @param canvas [ChunkyPNG::Image] The canvas image to render pixels on.
24
- def initialize(offset, canvas)
25
- @offset = offset
26
- @canvas = canvas
27
- end
28
-
29
- # @param y [Integer]
30
- # @param row [Diff::LCS:ContextChange]
31
- def render_row(y, row)
32
- if row.unchanged?
33
- render_unchanged_row(y, row)
34
- elsif row.deleting?
35
- render_deleted_row(y, row)
36
- elsif row.adding?
37
- render_added_row(y, row)
38
- else # changing?
39
- render_changed_row(y, row)
40
- end
41
- end
42
-
43
- # @param y [Integer]
44
- # @param row [Diff::LCS:ContextChange]
45
- def render_unchanged_row(y, row)
46
- row.new_element.each_with_index do |pixel, x|
47
- # Render the unchanged pixel as-is
48
- render_pixel(x, y, pixel)
49
- end
50
- end
51
-
52
- # @param y [Integer]
53
- # @param row [Diff::LCS:ContextChange]
54
- def render_changed_row(y, row)
55
- # no default implementation
56
- end
57
-
58
- # @param y [Integer]
59
- # @param row [Diff::LCS:ContextChange]
60
- def render_added_row(y, row)
61
- # no default implementation
62
- end
63
-
64
- # @param y [Integer]
65
- # @param row [Diff::LCS:ContextChange]
66
- def render_deleted_row(y, row)
67
- # no default implementation
68
- end
69
-
70
- # Compute a score that represents the difference between 2 pixels
71
- #
72
- # This method simply takes the Euclidean distance between the RGBA channels
73
- # of 2 colors over the maximum possible Euclidean distance. This gives us a
74
- # percentage of how different the two colors are.
75
- #
76
- # Although it would be more perceptually accurate to calculate a proper
77
- # Delta E in Lab colorspace, we probably don't need perceptual accuracy for
78
- # this application, and it is nice to avoid the overhead of converting RGBA
79
- # to Lab.
80
- #
81
- # @param pixel_after [Integer]
82
- # @param pixel_before [Integer]
83
- # @return [Float] number between 0 and 1 where 1 is completely different
84
- # and 0 is no difference
85
- def pixel_diff_score(pixel_after, pixel_before)
86
- ChunkyPNG::Color::euclidean_distance_rgba(pixel_after, pixel_before) /
87
- ChunkyPNG::Color::MAX_EUCLIDEAN_DISTANCE_RGBA
88
- end
89
-
90
- # @param diff_score [Float]
91
- # @return [Integer] a number between 0 and 255 that represents the alpha
92
- # channel of of the difference
93
- def diff_alpha(diff_score)
94
- (BASE_DIFF_ALPHA + ((255 - BASE_DIFF_ALPHA) * diff_score)).round
95
- end
96
-
97
- # Renders a pixel on the specified x and y position. Uses the offset that
98
- # the comparison image has been configured with.
99
- #
100
- # @param x [Integer]
101
- # @param y [Integer]
102
- # @param pixel [Integer]
103
- def render_pixel(x, y, pixel)
104
- @canvas.set_pixel(x + @offset, y, pixel)
105
- end
106
- end
107
- end
108
- end
@@ -1,34 +0,0 @@
1
- module Happo
2
- module SnapshotComparisonImage
3
- # This class renders a gutter-column with a color representing the type of
4
- # change that has happened.
5
- class Gutter < SnapshotComparisonImage::Base
6
- WIDTH = 10
7
- GRAY = ChunkyPNG::Color.from_hex '#cccccc'
8
-
9
- def render_row(y, row)
10
- WIDTH.times do |x|
11
- render_pixel(x, y, gutter_color(row))
12
- end
13
- # render a two-pixel empty column
14
- 2.times do |x|
15
- render_pixel(WIDTH - 1 - x, y, WHITE)
16
- end
17
- end
18
-
19
- private
20
-
21
- def gutter_color(row)
22
- if row.unchanged?
23
- WHITE
24
- elsif row.deleting?
25
- RED
26
- elsif row.adding?
27
- GREEN
28
- else # changed?
29
- GRAY
30
- end
31
- end
32
- end
33
- end
34
- end
@@ -1,88 +0,0 @@
1
- module Happo
2
- module SnapshotComparisonImage
3
- # This subclass of `SnapshotComparisonImage` knows how to overlay the
4
- # after-image on top of the before-image, and render the difference in a
5
- # scaled magenta color.
6
- class Overlayed < SnapshotComparisonImage::Base
7
- WHITE_OVERLAY = ChunkyPNG::Color.fade(WHITE, 1 - BASE_ALPHA)
8
-
9
- # @param offset [Integer]
10
- # @param canvas [ChunkyPNG::Image]
11
- # @see SnapshotComparisonImage::Base
12
- def initialize(offset, canvas)
13
- @diff_pixels = {}
14
- @faded_pixels = {}
15
- super
16
- end
17
-
18
- # @param y [Integer]
19
- # @param row [Diff::LCS:ContextChange]
20
- def render_unchanged_row(y, row)
21
- # Render translucent original pixels
22
- row.new_element.each_with_index do |pixel, x|
23
- render_faded_pixel(x, y, pixel)
24
- end
25
- end
26
-
27
- # @param y [Integer]
28
- # @param row [Diff::LCS:ContextChange]
29
- def render_deleted_row(y, row)
30
- row.old_element.each_with_index do |pixel_before, x|
31
- render_faded_magenta_pixel(TRANSPARENT, pixel_before, x, y)
32
- end
33
- end
34
-
35
- # @param y [Integer]
36
- # @param row [Diff::LCS:ContextChange]
37
- def render_added_row(y, row)
38
- row.new_element.each_with_index do |pixel_after, x|
39
- render_faded_magenta_pixel(pixel_after, TRANSPARENT, x, y)
40
- end
41
- end
42
-
43
- # @param y [Integer]
44
- # @param row [Diff::LCS:ContextChange]
45
- def render_changed_row(y, row)
46
- row.old_element.zip(row.new_element).each_with_index do |pixels, x|
47
- pixel_before, pixel_after = pixels
48
- render_faded_magenta_pixel(
49
- pixel_after || TRANSPARENT,
50
- pixel_before || TRANSPARENT,
51
- x, y)
52
- end
53
- end
54
-
55
- private
56
-
57
- # @param pixel_after [Integer]
58
- # @param pixel_before [Integer]
59
- # @param x [Integer]
60
- # @param y [Integer]
61
- def render_faded_magenta_pixel(pixel_after, pixel_before, x, y)
62
- score = pixel_diff_score(pixel_after, pixel_before)
63
- if score > 0
64
- render_diff_pixel(x, y, score)
65
- else
66
- render_faded_pixel(x, y, pixel_after)
67
- end
68
- end
69
-
70
- # @param x [Integer]
71
- # @param y [Integer]
72
- # @param score [Float]
73
- def render_diff_pixel(x, y, score)
74
- @diff_pixels[score] ||= compose_quick(fade(MAGENTA, diff_alpha(score)),
75
- WHITE)
76
- render_pixel(x, y, @diff_pixels[score])
77
- end
78
-
79
- # @param x [Integer]
80
- # @param y [Integer]
81
- # @param pixel [Integer]
82
- def render_faded_pixel(x, y, pixel)
83
- @faded_pixels[pixel] ||= compose_quick(WHITE_OVERLAY, pixel)
84
- render_pixel(x, y, @faded_pixels[pixel])
85
- end
86
- end
87
- end
88
- end