├── .editorconfig ├── .eslintrc.js ├── .github ├── .kodiak.toml ├── dependabot.yml └── workflows │ └── tests.yml ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── README.md ├── __tests__ └── integration │ ├── __snapshots__ │ ├── examples.js.snap │ └── getStats.js.snap │ ├── examples.js │ └── getStats.js ├── docs ├── _config.yml ├── art-direction.md ├── ga-custom-report.png ├── index.md ├── options.md ├── step1.md ├── step2.md └── step3.md ├── examples ├── nicolas-hoizey.com │ ├── configuration.js │ ├── no-avatar.png │ ├── page.html │ ├── run.sh │ ├── stats.csv │ ├── webpagetest-resilient-web-design.png │ └── what-does-my-site-cost.png └── simple │ ├── configuration.js │ ├── page.html │ ├── run.sh │ └── stats.csv ├── package-lock.json ├── package.json ├── prettier.config.js └── src ├── adjustDensitiesAndViewportsWithStats.js ├── browse.js ├── cli.js ├── getStats.js ├── index.js ├── logger.js └── spinner.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | extends: 'plugin:prettier/recommended', 7 | parserOptions: { 8 | ecmaVersion: 2017, 9 | sourceType: 'module', 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge] 4 | automerge_label = 'automerge' 5 | 6 | # https://kodiakhq.com/docs/recipes#better-merge-messages 7 | [merge.message] 8 | title = "pull_request_title" 9 | body = "pull_request_body" 10 | include_pr_number = true 11 | body_type = "markdown" 12 | strip_html_comments = true 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | versioning-strategy: lockfile-only 6 | schedule: 7 | interval: 'monthly' 8 | open-pull-requests-limit: 20 9 | labels: 10 | - 'dependencies' 11 | - 'automerge' 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI tests 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | 9 | - name: Get npm cache directory 10 | id: npm-cache 11 | run: | 12 | echo "::set-output name=dir::$(npm config get cache)" 13 | 14 | - name: Use cache 15 | uses: actions/cache@v2 16 | with: 17 | path: ${{ steps.npm-cache.outputs.dir }} 18 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 19 | restore-keys: ${{ runner.os }}-node- 20 | 21 | - name: Select Node.js version 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: '16' 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Run tests 30 | run: npm run test >> $GITHUB_STEP_SUMMARY 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Clever Age 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daltons 2 | 3 | [![Travis build status](https://img.shields.io/travis/cleverage/daltons.svg?style=popout)](https://travis-ci.org/cleverage/daltons) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/cleverage/daltons/badge.svg?targetFile=package.json)](https://snyk.io/test/github/cleverage/daltons?targetFile=package.json) 5 | [![License](https://img.shields.io/github/license/cleverage/daltons.svg?style=popout)](https://github.com/cleverage/daltons/blob/master/LICENSE.md) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons?ref=badge_shield) 7 | [![GitHub stars](https://img.shields.io/github/stars/cleverage/daltons.svg?style=social)](https://github.com/cleverage/daltons/stargazers) 8 | 9 | `daltons` is a command-line tool that computes optimal image widths to put in [`srcset`](https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset) attributes of [responsive images](https://responsiveimages.org/). 10 | 11 | ## Why do we need this tool? 12 | 13 | We want to provide the best experience to [our clients](https://www.clever-age.com/en/our-work/)’ users, so optimizing web performance is one of our main concerns. 14 | 15 | Using responsive images in every projects, we wanted to be able to make it as efficient as possible. The main difficulty is choosing the image widths we put in `srcset` attributes, because nothing in the standard tells us about this. 16 | 17 | ## How does it work? 18 | 19 | It takes 3 steps for `daltons` to find the best widths to put in the `srcset` attribute of a responsive image: 20 | 21 | - take Real User Monitoring (RUM) of viewport widths and screen densities 22 | - list the image’s widths across all of these viewport widths 23 | - compute the optimal image widths to put in the `srcset` attribute to cover all these needs 24 | 25 | Learn more in [the full documentation](https://cleverage.github.io/daltons/). 26 | 27 | ## Usage 28 | 29 | To install and run this application, you’ll need [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. 30 | 31 | From your command line, install `daltons` as a global package: 32 | 33 | ``` 34 | npm i -g github:cleverage/daltons 35 | ``` 36 | 37 | Then run it with the `-h` option to get help: 38 | 39 | ``` 40 | npx daltons -h 41 | ``` 42 | 43 | Or see detailed options in [the full documentation](https://cleverage.github.io/daltons/options.html) and look at [examples and use cases](https://cleverage.github.io/daltons/#examples-and-use-cases). 44 | 45 | ## Built with 46 | 47 | - [Node.js](https://nodejs.org/en/) 48 | - [Puppeteer](https://developers.google.com/web/tools/puppeteer/), a Node.js library which provides a high-level API to control headless Chrome 49 | 50 | ## Authors 51 | 52 | - [Nicolas Hoizey](https://github.com/nhoizey): Idea and initial work, maintainer 53 | - [Yvain Liechti](https://github.com/ryuran): Early contributor, maintainer 54 | 55 | See also the list of [contributors](https://github.com/cleverage/daltons/contributors) who participated in this project. 56 | 57 | ## License 58 | 59 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 60 | 61 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons?ref=badge_large) 62 | 63 | ## Related projects 64 | 65 | - [Sizer-Soze](https://blog.yoav.ws/who_is_sizer_soze/), developed by [Yoav Weiss](https://github.com/yoavweiss), “is a utility that enables you to evaluate how much you could save by properly resizing your images to match their display size on various viewports”. 66 | - [imaging-heap](https://github.com/filamentgroup/imaging-heap), developed by [Zach Leatherman](https://github.com/zachleat) is “a command line tool to measure the efficiency of your responsive image markup across viewport sizes and device pixel ratios”. 67 | -------------------------------------------------------------------------------- /__tests__/integration/__snapshots__/examples.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`examples simple example should return the list of perfect width for its image and stats 1`] = ` 4 | Array [ 5 | 1030, 6 | 520, 7 | 770, 8 | ] 9 | `; 10 | -------------------------------------------------------------------------------- /__tests__/integration/__snapshots__/getStats.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Stats parsing from CSV file example csv 1`] = ` 4 | Array [ 5 | Object { 6 | "density": 1, 7 | "viewport": 1020, 8 | "views": 35, 9 | }, 10 | Object { 11 | "density": 2, 12 | "viewport": 1021, 13 | "views": 3, 14 | }, 15 | Object { 16 | "density": 1, 17 | "viewport": 1022, 18 | "views": 53, 19 | }, 20 | Object { 21 | "density": 2, 22 | "viewport": 1023, 23 | "views": 12, 24 | }, 25 | Object { 26 | "density": 1, 27 | "viewport": 1024, 28 | "views": 23, 29 | }, 30 | Object { 31 | "density": 1.5, 32 | "viewport": 1025, 33 | "views": 27, 34 | }, 35 | Object { 36 | "density": 1, 37 | "viewport": 1026, 38 | "views": 28, 39 | }, 40 | Object { 41 | "density": 1.25, 42 | "viewport": 1027, 43 | "views": 32, 44 | }, 45 | Object { 46 | "density": 1, 47 | "viewport": 1028, 48 | "views": 7, 49 | }, 50 | Object { 51 | "density": 1, 52 | "viewport": 1029, 53 | "views": 18, 54 | }, 55 | Object { 56 | "density": 1.1, 57 | "viewport": 1030, 58 | "views": 23, 59 | }, 60 | ] 61 | `; 62 | -------------------------------------------------------------------------------- /__tests__/integration/examples.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const examplesPath = path.join(__dirname, '../../examples') 5 | 6 | describe('examples', () => { 7 | const SUT = require('../../src/index.js') 8 | 9 | // Declare tests automatically depending on the existing folders within ./examples 10 | // Use sync method in order to declare test cases synchronously 11 | const examples = fs.readdirSync(examplesPath) 12 | 13 | examples.forEach((exampleName) => { 14 | if ( 15 | fs.statSync(path.join(examplesPath, exampleName)).isDirectory() && 16 | exampleName != 'nicolas-hoizey.com' 17 | ) 18 | it(`${exampleName} example should return the list of perfect width for its image and stats`, async () => { 19 | expect.assertions(1) 20 | 21 | const configurationFile = path.join( 22 | examplesPath, 23 | `${exampleName}/configuration.js`, 24 | ) 25 | const config = require(configurationFile) 26 | const actualResults = await SUT(config) 27 | 28 | expect(actualResults).toMatchSnapshot() 29 | }, 600000) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /__tests__/integration/getStats.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const getStats = require('../../src/getStats') 3 | 4 | describe('Stats parsing from CSV file', () => { 5 | it(`example csv`, async () => { 6 | expect( 7 | getStats(path.resolve(__dirname, '../../examples/simple/stats.csv')), 8 | ).toMatchSnapshot() 9 | }, 200) 10 | }) 11 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/art-direction.md: -------------------------------------------------------------------------------- 1 | [< Back home](/daltons/) 2 | 3 | # Use case: Art Direction 4 | 5 | How to deal with multiple `` with `mix/max-width` media queries (Art Direction)? 6 | 7 | If you have some code like this: 8 | 9 | ```html 10 | 11 | 12 | … 13 | 14 | ``` 15 | 16 | You will have to run the script twice, with (at least) these parameters, to get widths for both `srcset`s: 17 | 18 | ```shell 19 | npx daltons --max-viewport 799 --verbose 20 | npx daltons --min-viewport 800 --verbose 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/ga-custom-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleverage/daltons/54df2bb52f6949ffc70cf4b629393c76644ed285/docs/ga-custom-report.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Daltons 2 | 3 | [![Travis build status](https://img.shields.io/travis/cleverage/daltons.svg?style=popout)](https://travis-ci.org/cleverage/daltons) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/cleverage/daltons/badge.svg?targetFile=package.json)](https://snyk.io/test/github/cleverage/daltons?targetFile=package.json) 5 | [![License](https://img.shields.io/github/license/cleverage/daltons.svg?style=popout)](https://github.com/cleverage/daltons/blob/master/LICENSE.md) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcleverage%2Fdaltons?ref=badge_shield) 7 | [![GitHub stars](https://img.shields.io/github/stars/cleverage/daltons.svg?style=social)](https://github.com/cleverage/daltons/stargazers) 8 | 9 | `daltons` is a command-line tool that computes optimal image widths to put in [`srcset`](https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset) attributes of [responsive images](https://responsiveimages.org/). 10 | 11 | ## Why do we need this tool? 12 | 13 | We want to provide the best experience to [our clients](https://www.clever-age.com/en/our-work/)’ users, so optimizing web performance is one of our main concerns. 14 | 15 | Using responsive images in every projects, we wanted to be able to make it as efficient as possible. The main difficulty is choosing the image widths we put in `srcset` attributes, because nothing in the standard tells us about this. 16 | 17 | ## How can we choose the responsive image widths? 18 | 19 | We didn’t invent anything here, we’re standing on the shoulders of giants. 20 | 21 | Back in 2015, [Jason Grigsby](https://cloudfour.com/is/jason-grigsby/) wrote this in [Responsive Images 101, Part 9: Image Breakpoints](https://cloudfour.com/thinks/responsive-images-101-part-9-image-breakpoints/): 22 | 23 | > we want to provide multiple image sources because of performance concerns, different screen densities, etc. [but] we can’t simply reuse our responsive layout breakpoints for our images. 24 | 25 | Jason presented a few ways to decide which image sizes to put in the `srcset` attribute of responsive images. 26 | 27 | [Cloudinary](https://cloudinary.com) then developed the [Responsive Image Breakpoints Generator](https://www.responsivebreakpoints.com/), based on Jason’s [setting image breakpoints based on a performance budget](https://cloudfour.com/thinks/responsive-images-101-part-9-image-breakpoints/#setting-image-breakpoints-based-on-a-performance-budget) idea. It was already a good optimization. 28 | 29 | > We’d start by defining a budget for the amount of wasted bytes that the browser would be allowed to download above what is needed to fit the size of the image in the page. 30 | 31 | But we believe the most efficient of Jason’s ideas is [setting image breakpoints based on most frequent requests](https://cloudfour.com/thinks/responsive-images-101-part-9-image-breakpoints/#setting-image-breakpoints-based-on-most-frequent-requests), inspired by a discussion with [Yoav Weiss](https://blog.yoav.ws/) from Akamai (who made [Blink and webkit support responsive images](https://blog.yoav.ws/by_the_people/) before joining Akamai) and [Ilya Grigorik](https://www.igvita.com/) from Google: 32 | 33 | > For these organizations, they can tie their image processing and breakpoints logic to their analytics and change the size of the images over time if they find that new image sizes are getting requested more frequently. 34 | 35 | Jason Grigsby also wrote in the same article that [humans shouldn’t be doing this](https://cloudfour.com/thinks/responsive-images-101-part-9-image-breakpoints/#humans-shouldnt-be-doing-this), and we agree. That’s why we started developing `daltons`. 36 | 37 | ## How does it work? 38 | 39 | It takes 3 steps for `daltons` to find the best widths to put in the `srcset` attribute of a responsive image: 40 | 41 | - [Step 1: take Real User Monitoring (RUM) of viewport widths and screen densities used on the website](/daltons/step1.html) 42 | - [Step 2: detect the image’s widths across all of these viewport widths](/daltons/step2.html) 43 | - [Step 3: compute the optimal image widths to put in the `srcset` attribute to cover all these needs](/daltons/step3.html) 44 | 45 | ## Getting started 46 | 47 | To install and run this application, you’ll need [Node.js](https://nodejs.org/en/download/) (which comes with [npm](http://npmjs.com)) installed on your computer. 48 | 49 | From your command line, install `daltons` as a global package: 50 | 51 | ``` 52 | npm install -g "cleverage/daltons" 53 | ``` 54 | 55 | Then run it with the `-h` option to get help: 56 | 57 | ``` 58 | npx daltons -h 59 | ``` 60 | 61 | It will output the following help: 62 | 63 | ``` 64 | Global: limit viewport widths, for example for Art Direction (see docs) 65 | --minViewport, -i Minimum viewport width to check [number] 66 | --maxViewport, -x Maximum viewport width to check [number] 67 | 68 | Step 1: get actual stats of site visitors 69 | --statsFile, -c File path from which reading the actual stats data in CSV format (screen density in dppx, viewport width in px, number of page views) [string] [required] 70 | 71 | Step 2: get variations of image width across viewport widths 72 | --url, -u Page URL [required] 73 | --selector, -s Image selector in the page [required] 74 | --delay, -d Delay after viewport resizing before checking image width (ms) [number] [default: 500] 75 | --variationsFile, -a File path to which saving the image width variations data, in CSV format [string] 76 | 77 | Step 3: compute optimal n widths from both datasets 78 | --widthsNumber, -n Number of widths to recommend [number] [default: 5] 79 | --destFile, -f File path to which saving the image widths for the srcset attribute [string] 80 | 81 | Options: 82 | --version Show version number [boolean] 83 | --verbose, -v Log progress and result in the console 84 | -h, --help Show help [boolean] 85 | 86 | Examples: 87 | npx cli.js --statsFile ./stats.csv --url 'https://example.com/' --selector 'main img[srcset]:first-of-type' --verbose 88 | npx cli.js -c ./stats.csv -u 'https://example.com/' -s 'main img[srcset]:first-of-type' -i 320 -x 1280 -a ./variations.csv -f ./srcset-widths.txt -v 89 | ``` 90 | 91 | See [details about each option](/daltons/options.html). 92 | 93 | ## Examples and use cases 94 | 95 | There are a few examples in the project’s repository: [examples](https://github.com/cleverage/daltons/tree/master/examples) 96 | 97 | Use cases: 98 | 99 | - [How to deal with multiple `` with `mix/max-width` media queries (Art Direction)](/daltons/art-direction.html) 100 | 101 | -------------------------------------------------------------------------------- /docs/options.md: -------------------------------------------------------------------------------- 1 | [< Back home](/daltons/) 2 | 3 | # Detailed options 4 | 5 | Quite a lot of options are necessary to run `daltons`, some being mandatory and other optional. 6 | 7 | Here are the details: 8 | 9 | ## Global options to limit viewport widths 10 | 11 | These options can be useful: 12 | 13 | - When some viewport width values taken from statistics are obviously out of range 14 | - For the [Art Direction](/daltons/art-direction.html) use case 15 | 16 | 19 | 20 | | option | alias | required? | type | default value | description | 21 | |-----------------|-------|-----------|--------|---------------|-------------------------------------------| 22 | | `--minViewport` | `-i` | optional | number | | Sets the minimum viewport width to check. | 23 | | `--maxViewport` | `-x` | optional | number | | Sets the maximum viewport width to check. | 24 | 25 | ## Step 1: get actual stats of site visitors 26 | 27 | | option | alias | required? | type | default value | description | 28 | |---------------|-------|-----------|-----------|---------------|----------------------------------------------------| 29 | | `--statsFile` | | required | file path | | File path from which reading the actual stats data | 30 | 31 | You need to provide a file with stats, which means statistics about viewport widths and screen density of the website’s visitors. 32 | 33 | See the details of [step 1](/daltons/step1.html). 34 | 35 | ## Step 2: get variations of image width across viewport widths 36 | 37 | | option | alias | required? | type | default value | description | 38 | |---------|-------|-----------|------|---------------|---------------------------------------------------------------------------------------| 39 | | `--url` | `-u` | required | URL | | Defines the URL of the page in which to check the image width across viewport widths. | 40 | 41 | ## Step 3: compute optimal n widths from both datasets 42 | -------------------------------------------------------------------------------- /docs/step1.md: -------------------------------------------------------------------------------- 1 | [< Back home](/daltons/) 2 | 3 | # Step 1: take Real User Monitoring (RUM) of viewport widths and screen densities used on the website 4 | 5 | ## Load stats listing page views with actual contexts (viewport, density) of site visitors 6 | 7 | You need to provide a file with statistics about viewport widths and screen densities of the website’s visitors. 8 | 9 | The stats file should be in [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) format, with these three columns: 10 | 11 | - viewport width in `px` 12 | - screen density in `dppx` 13 | - number of page views in this context 14 | 15 | There are a few requirements: 16 | 17 | - put column headers in first row 18 | - use a comma separator 19 | - don’t use any thousands separator 20 | - viewport width and number of page views are integers 21 | - screen density is a float using a dot as decimal separator 22 | 23 | See this example from the project’s repository: [stats.csv](https://github.com/cleverage/daltons/blob/master/examples/simple/stats.csv) 24 | 25 | ## Getting these data with an Analytics solution 26 | 27 | You should be able to feed custom dimensions to your Analytics solution. 28 | 29 | ### Computing values 30 | 31 | ```javascript 32 | // get device pixel ratio in dppx 33 | // https://github.com/ryanve/res/blob/master/src/index.js 34 | var screen_density = 35 | typeof window == 'undefined' 36 | ? 0 37 | : +window.devicePixelRatio || 38 | Math.sqrt(screen.deviceXDPI * screen.deviceYDPI) / 96 || 39 | 0 40 | // keep only 3 decimals: http://jsfiddle.net/AsRqx/ 41 | screen_density = +(Math.round(screen_density + 'e+3') + 'e-3') 42 | 43 | // get viewport width 44 | // http://stackoverflow.com/a/8876069/717195 45 | var viewport_width = Math.max( 46 | document.documentElement.clientWidth, 47 | window.innerWidth || 0, 48 | ) 49 | ``` 50 | 51 | ### Sending data to the analytics solution 52 | 53 | Here is the code to use to send these data to Google Analytics: 54 | 55 | ```javascript 56 | // Code to send these custom dimensions to Google Analytics 57 | ga('create', '', 'auto') 58 | 59 | ga('set', { 60 | dimension1: screen_density, 61 | dimension2: viewport_width, 62 | }) 63 | 64 | ga('send', 'pageview') 65 | ``` 66 | 67 | You will then have to get the data from your Analytics solution. Wait for a while to get accurate data, depending on your traffic. 68 | 69 | *Note: Google Analytics provides [a native browserSize variable](https://developers.google.com/analytics/devguides/reporting/core/dimsmets#view=detail&group=platform_or_device&jump=ga_browsersize), but it is a session-scoped dimension. We need pageview-scoped dimensions (hence `ga('send', 'pageview')`) because we will use these data to optimize image sizes for each page view. Viewport width (and screen density) of one user with multiple page views (actually browsing the site, more engaged) should influence the optimizations more than one user bouncing with one single page view, so sessions are not as accurate as page views.* 70 | 71 | ### Using collected data 72 | 73 | Here’s how to create a custom report in Google Analytics, for example: 74 | 75 | ![Creating a custom report in Google Analytics](ga-custom-report.png) 76 | -------------------------------------------------------------------------------- /docs/step2.md: -------------------------------------------------------------------------------- 1 | [< Back home](/daltons/) 2 | 3 | # Step 2: detect the image’s widths across all of these viewport widths 4 | 5 | A script running a headless Chrome with [Puppeteer](https://developers.google.com/web/tools/puppeteer/) will get the CSS width of an image on every relevant viewport widths. 6 | -------------------------------------------------------------------------------- /docs/step3.md: -------------------------------------------------------------------------------- 1 | [< Back home](/daltons/) 2 | 3 | # Step 3: compute the optimal image widths to put in the `srcset` attribute to cover all these needs 4 | 5 | Computing both datasets will help define optimal widths for the `srcset` attribute of the responsive image. 6 | -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | statsFile: `${__dirname}/stats.csv`, 3 | url: `file://${__dirname}/page.html`, 4 | selector: '.main img[srcset]:first-of-type', 5 | maxViewport: 1600, 6 | delay: 20, 7 | } 8 | -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/no-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleverage/daltons/54df2bb52f6949ffc70cf4b629393c76644ed285/examples/nicolas-hoizey.com/no-avatar.png -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | path=$(pwd) 3 | node ../../src/cli.js --stats-file ./stats.csv --url "file://${path}/page.html" --selector '.main img[srcset]:first-of-type' --max-viewport 1600 --delay 5 --verbose 4 | -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/stats.csv: -------------------------------------------------------------------------------- 1 | Viewport width,Screen density,Pageviews 2 | 1920,1,3505 3 | 1440,2,1341 4 | 1366,1,1194 5 | 375,2,725 6 | 360,3,587 7 | 2560,1,461 8 | 1536,1.25,418 9 | 1680,2,411 10 | 1280,1,352 11 | 1280,2,344 12 | 1440,1,334 13 | 1600,1,312 14 | 412,2.625,260 15 | 360,2,251 16 | 1680,1,236 17 | 320,2,221 18 | 375,3,164 19 | 414,3,161 20 | 1920,2,146 21 | 1280,1.5,128 22 | 1024,2,93 23 | 1855,1,90 24 | 1922,1,83 25 | 2560,2,80 26 | 1858,1,76 27 | 360,4,75 28 | 1918,1,64 29 | 768,2,63 30 | 800,1,59 31 | 1024,1,50 32 | 412,3.5,50 33 | 1853,1,46 34 | 1360,1,45 35 | 1301,1,38 36 | 1880,1,36 37 | 1391,1,35 38 | 1679,2,35 39 | 360,1.5,32 40 | 1439,2,30 41 | 1536,2.5,27 42 | 2048,1,27 43 | 2048,1.25,27 44 | 1014,1,26 45 | 1200,1,24 46 | 393,2.75,24 47 | 1278,1,23 48 | 1326,1,23 49 | 1910,1,23 50 | 1914,1,22 51 | 1916,1,22 52 | 1919,1,21 53 | 320,1.5,21 54 | 1280,1.25,20 55 | 2048,2,20 56 | 1112,2,19 57 | 1281,2,17 58 | 2560,1.5,17 59 | 958,1,17 60 | 1393,2,16 61 | 1270,2,15 62 | 1276,1,15 63 | 1299,1,15 64 | 424,2.55,15 65 | 1279,2,14 66 | 1321,1,14 67 | 1366,2,14 68 | 1368,2,14 69 | 1538,1,14 70 | 1904,1,14 71 | 1912,1,14 72 | 1040,1,13 73 | 1522,1,13 74 | 1617,1,13 75 | 1720,1,13 76 | 1842,1,13 77 | 1853,2,13 78 | 2195,1.75,13 79 | 3440,1,13 80 | 414,2.609,13 81 | 1278,2,12 82 | 1396,2,12 83 | 1409,2,12 84 | 1436,1,12 85 | 1440,1.5,12 86 | 1863,1,12 87 | 1921,1,12 88 | 1200,2,11 89 | 1264,1,11 90 | 1276,2,11 91 | 1379,1,11 92 | 1438,1,11 93 | 1439,1,11 94 | 1534,1.25,11 95 | 1535,1,11 96 | 1600,2,11 97 | 1690,1,11 98 | 1865,1,11 99 | 1264,2,10 100 | 1277,2,10 101 | 1309,1,10 102 | 1324,2,10 103 | 1354,1,10 104 | 1387,2,10 105 | 1435,2,10 106 | 1436,2,10 107 | 1518,0.9,10 108 | 1620,2,10 109 | 1869,1,10 110 | 587,1,10 111 | 834,2,10 112 | 1080,1,9 113 | 1150,2,9 114 | 1270,1,9 115 | 1282,1,9 116 | 1317,1,9 117 | 1319,1,9 118 | 1324,1,9 119 | 1362,1,9 120 | 1415,2,9 121 | 1428,1,9 122 | 1437,2,9 123 | 1438,2,9 124 | 1485,1,9 125 | 1533,1,9 126 | 1615,1,9 127 | 1640,1,9 128 | 1642,2,9 129 | 1679,1,9 130 | 600,2,9 131 | 960,2,9 132 | 1021,2,8 133 | 1206,1,8 134 | 1277,1,8 135 | 1280,2.5,8 136 | 1344,1.25,8 137 | 1378,1,8 138 | 1380,1,8 139 | 1434,2,8 140 | 1455,1,8 141 | 1500,2,8 142 | 1518,2,8 143 | 1538,1.25,8 144 | 1539,2,8 145 | 1622,1,8 146 | 1658,2,8 147 | 1670,1,8 148 | 1719,1,8 149 | 1813,1,8 150 | 1852,1,8 151 | 1867,1,8 152 | 1915,1,8 153 | 1924,1,8 154 | 2400,0.8,8 155 | 2493,1,8 156 | 2557,1,8 157 | 408,3.529,8 158 | 455,2.375,8 159 | 950,1,8 160 | 1239,2,7 161 | 1266,1,7 162 | 1273,1,7 163 | 1283,2,7 164 | 1294,1,7 165 | 1336,1,7 166 | 1338,1,7 167 | 1350,1,7 168 | 1397,2,7 169 | 1441,2,7 170 | 1521,1,7 171 | 1540,1.25,7 172 | 1571,1,7 173 | 1595,2,7 174 | 1707,1.5,7 175 | 1713,1,7 176 | 1861,1,7 177 | 1903,1,7 178 | 1911,1,7 179 | 1919,2,7 180 | 2559,1,7 181 | 3840,1,7 182 | 571,1,7 183 | 968,2,7 184 | 1065,1,6 185 | 1192,1,6 186 | 1194,1,6 187 | 1218,2,6 188 | 1232,2,6 189 | 1235,1,6 190 | 1240,1,6 191 | 1240,2,6 192 | 1245,1,6 193 | 1340,1,6 194 | 1358,1,6 195 | 1365,1,6 196 | 1371,1,6 197 | 1372,1,6 198 | 1382,2,6 199 | 1395,2,6 200 | 1467,1,6 201 | 1481,1,6 202 | 1492,1,6 203 | 1501,1,6 204 | 1507,1,6 205 | 1535,1.25,6 206 | 1550,1,6 207 | 1551,2,6 208 | 1563,1,6 209 | 1576,1,6 210 | 1584,1,6 211 | 1625,2,6 212 | 1630,2,6 213 | 1678,2,6 214 | 1681,2,6 215 | 1707,1,6 216 | 1718,1,6 217 | 1733,1,6 218 | 1745,1.1,6 219 | 1766,1,6 220 | 1789,1,6 221 | 1873,1,6 222 | 1892,1,6 223 | 1894,1,6 224 | 1896,1,6 225 | 1900,1,6 226 | 1913,1,6 227 | 320,4.5,6 228 | 411,1,6 229 | 432,2.5,6 230 | 800,2,6 231 | 831,2,6 232 | 840,2,6 233 | 957,1,6 234 | 1000,1,5 235 | 1009,2,5 236 | 1015,2,5 237 | 1027,1,5 238 | 1089,1,5 239 | 1113,2,5 240 | 1130,2,5 241 | 1142,2,5 242 | 1199,1,5 243 | 1205,1,5 244 | 1226,1,5 245 | 1234,2,5 246 | 1241,1,5 247 | 1254,1,5 248 | 1256,2,5 249 | 1268,1,5 250 | 1273,2,5 251 | 1275,1,5 252 | 1275,2,5 253 | 1279,1,5 254 | 1303,2,5 255 | 1304,1,5 256 | 1313,1,5 257 | 1315,1,5 258 | 1323,1,5 259 | 1333,1,5 260 | 1334,1,5 261 | 1334,2,5 262 | 1335,1,5 263 | 1347,1,5 264 | 1353,2,5 265 | 1357,2,5 266 | 1364,1,5 267 | 1378,2,5 268 | 1388,2,5 269 | 1389,2,5 270 | 1391,2,5 271 | 1392,2,5 272 | 1403,1,5 273 | 1407,2,5 274 | 1410,2,5 275 | 1417,2,5 276 | 1418,2,5 277 | 1421,1,5 278 | 1422,1,5 279 | 1427,1,5 280 | 1429,2,5 281 | 1430,1,5 282 | 1434,1,5 283 | 1441,1,5 284 | 1442,2,5 285 | 1451,1,5 286 | 1456,2,5 287 | 1459,1,5 288 | 1482,1,5 289 | 1503,1,5 290 | 1504,1,5 291 | 1504,2,5 292 | 1507,2,5 293 | 1516,1,5 294 | 1535,2,5 295 | 1536,1,5 296 | 1563,2,5 297 | 1573,2,5 298 | 1575,1,5 299 | 1592,1,5 300 | 1594,1,5 301 | 1597,1,5 302 | 1606,1,5 303 | 1618,1,5 304 | 1627,1,5 305 | 1629,1,5 306 | 1636,2,5 307 | 1641,2,5 308 | 1644,1,5 309 | 1649,1,5 310 | 1657,1,5 311 | 1676,1,5 312 | 1704,1,5 313 | 1708,1,5 314 | 1726,1,5 315 | 1736,2,5 316 | 1742,1,5 317 | 1760,1,5 318 | 1777,1,5 319 | 1779,2,5 320 | 1787,1,5 321 | 1808,1,5 322 | 1848,1,5 323 | 1854,1,5 324 | 1862,1,5 325 | 1871,1,5 326 | 1875,1,5 327 | 1877,1,5 328 | 1879,1,5 329 | 1886,1,5 330 | 1901,1,5 331 | 1902,1,5 332 | 1916,2,5 333 | 2430,2,5 334 | 3200,1,5 335 | 376,2.875,5 336 | 412,1.75,5 337 | 424,3.4,5 338 | 640,2,5 339 | 720,2,5 340 | 839,2,5 341 | 1008,1,4 342 | 1021,1,4 343 | 1033,1,4 344 | 1058,2,4 345 | 1064,1,4 346 | 1080,2,4 347 | 1085,1,4 348 | 1089,2,4 349 | 1120,1,4 350 | 1150,1,4 351 | 1178,1,4 352 | 1191,1,4 353 | 1196,1,4 354 | 1198,1,4 355 | 1206,2,4 356 | 1212,1,4 357 | 1230,2,4 358 | 1231,2,4 359 | 1233,2,4 360 | 1236,2,4 361 | 1244,1,4 362 | 1248,2,4 363 | 1251,1,4 364 | 1258,2,4 365 | 1260,2,4 366 | 1262,1,4 367 | 1270,1.5,4 368 | 1274,2,4 369 | 1279,1.5,4 370 | 1284,1,4 371 | 1284,1.5,4 372 | 1289,1,4 373 | 1290,1,4 374 | 1291,2,4 375 | 1296,2,4 376 | 1311,1,4 377 | 1322,2,4 378 | 1329,1,4 379 | 1331,1,4 380 | 1333,2,4 381 | 1341,1,4 382 | 1347,2,4 383 | 1353,1,4 384 | 1355,1,4 385 | 1361,2,4 386 | 1363,1,4 387 | 1367,1,4 388 | 1372,1.4,4 389 | 1375,1,4 390 | 1384,2,4 391 | 1386,1,4 392 | 1389,1,4 393 | 1393,1,4 394 | 1397,1,4 395 | 1399,2,4 396 | 1403,2,4 397 | 1405,2,4 398 | 1407,1,4 399 | 1409,1,4 400 | 1410,1,4 401 | 1412,1,4 402 | 1413,1,4 403 | 1418,1,4 404 | 1422,2,4 405 | 1424,1,4 406 | 1425,2,4 407 | 1430,2,4 408 | 1431,2,4 409 | 1435,1,4 410 | 1437,1,4 411 | 1442,1,4 412 | 1454,1,4 413 | 1457,1,4 414 | 1463,2,4 415 | 1469,2,4 416 | 1476,1,4 417 | 1490,1,4 418 | 1498,1,4 419 | 1501,2,4 420 | 1505,2,4 421 | 1516,2,4 422 | 1525,0.896,4 423 | 1527,1,4 424 | 1529,1,4 425 | 1534,1,4 426 | 1551,1,4 427 | 1554,1,4 428 | 1556,1,4 429 | 1558,1,4 430 | 1569,1,4 431 | 1569,2,4 432 | 1572,1,4 433 | 1574,1,4 434 | 1574,2,4 435 | 1596,1,4 436 | 1613,1,4 437 | 1621,1,4 438 | 1625,1,4 439 | 1626,2,4 440 | 1631,2,4 441 | 1635,2,4 442 | 1638,1,4 443 | 1638,2,4 444 | 1642,1,4 445 | 1658,1,4 446 | 1661,1,4 447 | 1664,1,4 448 | 1666,1,4 449 | 1676,2,4 450 | 1682,1,4 451 | 1682,2,4 452 | 1686,1,4 453 | 1694,1,4 454 | 1696,1,4 455 | 1701,1,4 456 | 1702,1,4 457 | 1703,1,4 458 | 1707,2.25,4 459 | 1717,1,4 460 | 1740,1.104,4 461 | 1773,1,4 462 | 1785,1,4 463 | 1801,0.75,4 464 | 1806,1,4 465 | 1811,1,4 466 | 1814,1,4 467 | 1821,1,4 468 | 1847,1,4 469 | 1881,1,4 470 | 1885,1,4 471 | 1893,1,4 472 | 1899,1,4 473 | 1908,1,4 474 | 1909,1,4 475 | 1917,1,4 476 | 1931,1,4 477 | 2008,1,4 478 | 2009,1,4 479 | 2050,1.25,4 480 | 2222,1,4 481 | 2331,2,4 482 | 2439,1,4 483 | 2498,1,4 484 | 2500,1,4 485 | 2556,1,4 486 | 2562,1,4 487 | 272,1,4 488 | 3008,2,4 489 | 377,3.825,4 490 | 400,1.8,4 491 | 424,1.7,4 492 | 864,2,4 493 | 902,1,4 494 | 944,1,4 495 | 952,1,4 496 | 1016,1,3 497 | 1023,1,3 498 | 1024,1.25,3 499 | 1034,1,3 500 | 1045,2,3 501 | 1046,1,3 502 | 1058,1,3 503 | 1088,1,3 504 | 1098,1,3 505 | 1099,1,3 506 | 1121,1,3 507 | 1124,2,3 508 | 1126,2,3 509 | 1133,2,3 510 | 1135,1,3 511 | 1145,2,3 512 | 1153,2,3 513 | 1161,1,3 514 | 1162,1,3 515 | 1165,1,3 516 | 1169,1,3 517 | 1172,1,3 518 | 1173,2,3 519 | 1181,2,3 520 | 1185,2,3 521 | 1191,2,3 522 | 1198,2,3 523 | 1201,1,3 524 | 1209,2,3 525 | 1211,2,3 526 | 1214,1,3 527 | 1230,1,3 528 | 1235,2,3 529 | 1237,1,3 530 | 1247,1,3 531 | 1247,2,3 532 | 1250,2,3 533 | 1252,2,3 534 | 1255,1,3 535 | 1255,2,3 536 | 1257,2,3 537 | 1258,1,3 538 | 1259,1,3 539 | 1263,1,3 540 | 1265,1,3 541 | 1268,2,3 542 | 1269,1,3 543 | 1272,1,3 544 | 1280,3,3 545 | 1282,1.5,3 546 | 1290,2,3 547 | 1294,2,3 548 | 1295,1,3 549 | 1298,2,3 550 | 1300,2,3 551 | 1302,1,3 552 | 1306,1,3 553 | 1310,2,3 554 | 1314,2,3 555 | 1328,1,3 556 | 1330,1,3 557 | 1330,2,3 558 | 1331,1.5,3 559 | 1337,1,3 560 | 1339,1,3 561 | 1344,2,3 562 | 1345,2,3 563 | 1348,1,3 564 | 1351,2,3 565 | 1354,2,3 566 | 1356,1,3 567 | 1357,1,3 568 | 1359,1,3 569 | 1360,2,3 570 | 1362,2,3 571 | 1363,2,3 572 | 1365,2,3 573 | 1369,1,3 574 | 1372,2,3 575 | 1373,2,3 576 | 1385,1,3 577 | 1386,2,3 578 | 1390,1,3 579 | 1390,2,3 580 | 1394,1,3 581 | 1395,1,3 582 | 1398,1,3 583 | 1400,1,3 584 | 1401,2,3 585 | 1404,2,3 586 | 1412,2,3 587 | 1415,1.3,3 588 | 1416,1,3 589 | 1416,2,3 590 | 1423,2,3 591 | 1426,1,3 592 | 1429,1,3 593 | 1444,2,3 594 | 1445,1,3 595 | 1452,1,3 596 | 1456,1,3 597 | 1458,1,3 598 | 1459,2,3 599 | 1468,1,3 600 | 1470,1,3 601 | 1479,2,3 602 | 1481,2,3 603 | 1484,1,3 604 | 1487,1,3 605 | 1493,2,3 606 | 1494,1,3 607 | 1494,2,3 608 | 1495,1,3 609 | 1497,2,3 610 | 1500,1,3 611 | 1502,2,3 612 | 1503,2,3 613 | 1509,2,3 614 | 1511,1,3 615 | 1512,2,3 616 | 1514,1,3 617 | 1515,2,3 618 | 1519,1,3 619 | 1522,1.25,3 620 | 1526,1.25,3 621 | 1528,1,3 622 | 1529,2,3 623 | 1540,2,3 624 | 1541,1,3 625 | 1542,1,3 626 | 1546,2,3 627 | 1547,1,3 628 | 1548,1,3 629 | 1549,1,3 630 | 1552,1,3 631 | 1553,1,3 632 | 1561,1,3 633 | 1562,1,3 634 | 1564,1,3 635 | 1564,2,3 636 | 1568,1,3 637 | 1578,1,3 638 | 1585,2,3 639 | 1587,1,3 640 | 1587,2,3 641 | 1591,1,3 642 | 1592,2,3 643 | 1596,1.5,3 644 | 1600,0.9,3 645 | 1602,1,3 646 | 1604,1,3 647 | 1611,1,3 648 | 1612,1,3 649 | 1612,2,3 650 | 1613,2,3 651 | 1616,1,3 652 | 1620,1,3 653 | 1623,1,3 654 | 1624,1,3 655 | 1630,1,3 656 | 1634,2,3 657 | 1637,1,3 658 | 1639,2,3 659 | 1648,2,3 660 | 1651,2,3 661 | 1653,1,3 662 | 1655,2,3 663 | 1671,1,3 664 | 1671,2,3 665 | 1672,1,3 666 | 1673,1,3 667 | 1673,2,3 668 | 1677,2,3 669 | 1678,1,3 670 | 1684,1,3 671 | 1692,1,3 672 | 1705,2,3 673 | 1709,1,3 674 | 1715,1,3 675 | 1715,1.75,3 676 | 1722,1,3 677 | 1725,2,3 678 | 1727,2,3 679 | 1728,1,3 680 | 1739,1,3 681 | 1741,1,3 682 | 1745,2,3 683 | 1746,1,3 684 | 1749,1,3 685 | 1759,2,3 686 | 1763,1,3 687 | 1778,1,3 688 | 1779,1,3 689 | 1793,1,3 690 | 1795,1,3 691 | 1796,1,3 692 | 1800,1,3 693 | 1810,1,3 694 | 1814,2,3 695 | 1816,1,3 696 | 1817,1,3 697 | 1820,1,3 698 | 1828,1,3 699 | 1833,1,3 700 | 1839,1,3 701 | 1840,1,3 702 | 1841,1,3 703 | 1843,1,3 704 | 1846,1,3 705 | 1851,1,3 706 | 1856,1,3 707 | 1857,1,3 708 | 1860,1,3 709 | 1867,1.8,3 710 | 1867,2,3 711 | 1872,1,3 712 | 1876,1,3 713 | 1876,2,3 714 | 1902,2,3 715 | 1906,1,3 716 | 1917,2,3 717 | 1920,1.25,3 718 | 1926,1,3 719 | 1928,1,3 720 | 1935,1,3 721 | 1973,1,3 722 | 2000,1,3 723 | 2016,1,3 724 | 2036,1,3 725 | 2052,1,3 726 | 2092,1,3 727 | 2144,0.896,3 728 | 2153,2,3 729 | 2160,1,3 730 | 2212,1,3 731 | 2304,2,3 732 | 2495,1,3 733 | 2501,1,3 734 | 2505,1,3 735 | 2543,1,3 736 | 2558,1,3 737 | 2559,2,3 738 | 320,3.375,3 739 | 3442,1,3 740 | 400,2,3 741 | 485,2.975,3 742 | 601,1.331,3 743 | 681,1,3 744 | 739,2.75,3 745 | 798,1,3 746 | 800,1.5,3 747 | 826,1,3 748 | 853,1,3 749 | 897,1,3 750 | 899,1,3 751 | 945,1,3 752 | 960,1,3 753 | 967,1,3 754 | 995,1,3 755 | 1012,1,2 756 | 1012,2,2 757 | 10240,0.25,2 758 | 1036,2.5,2 759 | 1038,1,2 760 | 1056,1,2 761 | 1060,1,2 762 | 1062,2,2 763 | 1067,1,2 764 | 1075,1,2 765 | 1076,2,2 766 | 1077,1,2 767 | 1077,2,2 768 | 1078,1,2 769 | 1079,1,2 770 | 1092,1,2 771 | 1095,1,2 772 | 1100,1,2 773 | 1100,2,2 774 | 1105,1,2 775 | 1109,1,2 776 | 1110,1.1,2 777 | 1111,1,2 778 | 1112,1,2 779 | 1120,2,2 780 | 1125,1,2 781 | 1129,2,2 782 | 1136,2,2 783 | 1137,1,2 784 | 1138,2,2 785 | 1140,1,2 786 | 1140,2,2 787 | 1141,1,2 788 | 1144,1,2 789 | 1146,1,2 790 | 1148,2,2 791 | 1151,2,2 792 | 1152,1,2 793 | 1155,1,2 794 | 1155,2,2 795 | 1160,1,2 796 | 1169,2,2 797 | 1171,2,2 798 | 1173,1.091,2 799 | 1177,1,2 800 | 1179,2,2 801 | 1180,1,2 802 | 1184,2,2 803 | 1186,1,2 804 | 1187,2,2 805 | 1188,1,2 806 | 1188,2,2 807 | 1190,2,2 808 | 1192,2,2 809 | 1196,2,2 810 | 1197,2,2 811 | 1199,2,2 812 | 1202,1,2 813 | 1203,2,2 814 | 1208,2,2 815 | 1209,1,2 816 | 1210,1,2 817 | 1210,2,2 818 | 1213,1,2 819 | 1215,1,2 820 | 1215,2,2 821 | 1217,2,2 822 | 1218,1.5,2 823 | 1219,2,2 824 | 1225,2,2 825 | 1227,2,2 826 | 1228,1,2 827 | 1229,1.563,2 828 | 1232,1,2 829 | 1233,1,2 830 | 1238,1,2 831 | 1239,1,2 832 | 1242,1,2 833 | 1245,2,2 834 | 1246,1,2 835 | 1246,2,2 836 | 1252,1,2 837 | 1253,1,2 838 | 1253,2,2 839 | 1256,1,2 840 | 1257,1,2 841 | 1261,1,2 842 | 1267,2,2 843 | 1272,2,2 844 | 1274,1,2 845 | 1281,1,2 846 | 1283,1,2 847 | 1284,2,2 848 | 1285,1,2 849 | 1286,1,2 850 | 1287,1,2 851 | 1291,1,2 852 | 1292,1,2 853 | 1292,2,2 854 | 1293,1,2 855 | 1295,2,2 856 | 1296,1,2 857 | 1297,2,2 858 | 1298,1,2 859 | 1298,1.25,2 860 | 1301,1.05,2 861 | 1303,1,2 862 | 1307,1,2 863 | 1307,2,2 864 | 1308,1,2 865 | 1308,1.75,2 866 | 1311,2,2 867 | 1314,1,2 868 | 1319,2,2 869 | 1320,1,2 870 | 1320,2,2 871 | 1322,1,2 872 | 1329,2,2 873 | 1331,2,2 874 | 1332,1,2 875 | 1332,2,2 876 | 1334,1.25,2 877 | 1335,2,2 878 | 1336,2,2 879 | 1341,2,2 880 | 1344,1,2 881 | 1345,1,2 882 | 1346,1,2 883 | 1352,1,2 884 | 1358,2,2 885 | 1359,2,2 886 | 1361,1,2 887 | 1364,2,2 888 | 1368,1,2 889 | 1368,1.25,2 890 | 1369,2,2 891 | 1370,1,2 892 | 1371,2,2 893 | 1373,1,2 894 | 1374,1,2 895 | 1374,2,2 896 | 1376,1.396,2 897 | 1381,1,2 898 | 1381,2,2 899 | 1382,1,2 900 | 1383,1,2 901 | 1383,2,2 902 | 1385,2,2 903 | 1387,1,2 904 | 1392,1,2 905 | 1396,1,2 906 | 1399,1,2 907 | 1401,1,2 908 | 1404,1,2 909 | 1408,1,2 910 | 1408,1.364,2 911 | 1408,2,2 912 | 1411,2,2 913 | 1413,2,2 914 | 1414,1,2 915 | 1417,1,2 916 | 1419,1,2 917 | 1421,2,2 918 | 1422,1.8,2 919 | 1423,1,2 920 | 1427,2,2 921 | 1428,2,2 922 | 1432,1,2 923 | 1433,1,2 924 | 1438,0.95,2 925 | 1444,1,2 926 | 1446,2,2 927 | 1448,2,2 928 | 1449,2,2 929 | 1460,2,2 930 | 1461,1,2 931 | 1462,1,2 932 | 1463,1,2 933 | 1465,1,2 934 | 1467,1.091,2 935 | 1472,1,2 936 | 1472,1.304,2 937 | 1474,1,2 938 | 1474,1.25,2 939 | 1475,1,2 940 | 1477,1,2 941 | 1478,1,2 942 | 1479,1.925,2 943 | 1480,1,2 944 | 1483,1.25,2 945 | 1488,1,2 946 | 1488,2,2 947 | 1493,1,2 948 | 1496,1,2 949 | 1496,2,2 950 | 1499,2,2 951 | 1502,1,2 952 | 1506,1,2 953 | 1510,2,2 954 | 1511,2,2 955 | 1512,1,2 956 | 1514,2,2 957 | 1515,1,2 958 | 1517,1,2 959 | 1518,1,2 960 | 1519,2,2 961 | 1523,1,2 962 | 1524,2.5,2 963 | 1525,1,2 964 | 1525,2,2 965 | 1527,2,2 966 | 1530,1,2 967 | 1532,1,2 968 | 1532,2,2 969 | 1536,2,2 970 | 1540,1,2 971 | 1541,2,2 972 | 1545,1,2 973 | 1546,1,2 974 | 1555,1,2 975 | 1558,2,2 976 | 1559,1,2 977 | 1565,2,2 978 | 1566,2,2 979 | 1573,1,2 980 | 1576,2,2 981 | 1578,2,2 982 | 1586,1,2 983 | 1588,1,2 984 | 1588,2,2 985 | 1589,2,2 986 | 1590,1,2 987 | 1593,1,2 988 | 1595,1,2 989 | 1598,1,2 990 | 1605,1,2 991 | 1609,1,2 992 | 1614,1,2 993 | 1614,2,2 994 | 1616,2,2 995 | 1619,2,2 996 | 1626,1,2 997 | 1627,2,2 998 | 1628,2,2 999 | 1632,2,2 1000 | 1633,1,2 1001 | 1633,2,2 1002 | 1638,2.5,2 1003 | 1640,2,2 1004 | 1642,1.17,2 1005 | 1643,1,2 1006 | 1645,1,2 1007 | 1646,2,2 1008 | 1648,1,2 1009 | 1650,2,2 1010 | 1651,1,2 1011 | 1657,2,2 1012 | 1658,1.65,2 1013 | 1660,1,2 1014 | 1660,2,2 1015 | 1662,2,2 1016 | 1664,1.154,2 1017 | 1665,1,2 1018 | 1665,2,2 1019 | 1666,1.5,2 1020 | 1668,1,2 1021 | 1668,2,2 1022 | 1669,2,2 1023 | 1672,2,2 1024 | 1677,1,2 1025 | 1681,1,2 1026 | 1687,2,2 1027 | 1695,1,2 1028 | 1698,2,2 1029 | 1706,1,2 1030 | 1706,2,2 1031 | 1707,0.938,2 1032 | 1708,2,2 1033 | 1710,1,2 1034 | 1711,1,2 1035 | 1714,1,2 1036 | 1715,2,2 1037 | 1721,1,2 1038 | 1735,1,2 1039 | 1737,1,2 1040 | 1743,1,2 1041 | 1747,1,2 1042 | 1751,1,2 1043 | 1753,1,2 1044 | 1755,1,2 1045 | 1756,1,2 1046 | 1757,1,2 1047 | 1758,1,2 1048 | 1759,1,2 1049 | 1761,1,2 1050 | 1763,2,2 1051 | 1764,1,2 1052 | 1764,2,2 1053 | 1767,1,2 1054 | 1775,1,2 1055 | 1776,1,2 1056 | 1780,1,2 1057 | 1783,1,2 1058 | 1783,2,2 1059 | 1784,1,2 1060 | 1788,2,2 1061 | 1790,1,2 1062 | 1792,1,2 1063 | 1794,1,2 1064 | 1801,1,2 1065 | 1802,1,2 1066 | 1805,1,2 1067 | 1810,2,2 1068 | 1812,2,2 1069 | 1818,1,2 1070 | 1819,1,2 1071 | 1824,1,2 1072 | 1825,1,2 1073 | 1826,1,2 1074 | 1827,1,2 1075 | 1829,1.05,2 1076 | 1832,1,2 1077 | 1844,1,2 1078 | 1845,1,2 1079 | 1849,1,2 1080 | 1850,1,2 1081 | 1859,1,2 1082 | 1859,2,2 1083 | 1864,1,2 1084 | 1873,2,2 1085 | 1874,2,2 1086 | 1877,2,2 1087 | 1878,1,2 1088 | 1879,2,2 1089 | 1880,2,2 1090 | 1882,2,2 1091 | 1884,1,2 1092 | 1888,1,2 1093 | 1891,1,2 1094 | 1897,1,2 1095 | 1905,1,2 1096 | 1906,2,2 1097 | 1908,2,2 1098 | 1910,2,2 1099 | 1925,2,2 1100 | 1927,1,2 1101 | 1935,2,2 1102 | 1939,1,2 1103 | 1948,1,2 1104 | 1951,2,2 1105 | 1976,1,2 1106 | 1978,1,2 1107 | 1986,1,2 1108 | 2029,1,2 1109 | 2047,2,2 1110 | 2054,2,2 1111 | 2069,1,2 1112 | 2080,1,2 1113 | 2087,1,2 1114 | 2095,2,2 1115 | 2096,1,2 1116 | 2102,1,2 1117 | 2103,2,2 1118 | 2140,1,2 1119 | 2144,1,2 1120 | 2166,1,2 1121 | 2168,1,2 1122 | 2174,1,2 1123 | 2182,1.76,2 1124 | 2199,1.75,2 1125 | 2205,2,2 1126 | 2209,1,2 1127 | 2248,1,2 1128 | 2250,2,2 1129 | 2270,1,2 1130 | 2275,1,2 1131 | 2277,0.6,2 1132 | 2285,2,2 1133 | 2297,1,2 1134 | 2300,1,2 1135 | 2344,1,2 1136 | 2393,1,2 1137 | 2432,1,2 1138 | 2438,1,2 1139 | 2452,1,2 1140 | 2475,1.034,2 1141 | 2493,2,2 1142 | 2494,2,2 1143 | 2499,1,2 1144 | 2503,1,2 1145 | 2522,1,2 1146 | 2527,1,2 1147 | 2532,1,2 1148 | 2544,1,2 1149 | 2550,1,2 1150 | 2552,1.5,2 1151 | 2555,1,2 1152 | 2557,2,2 1153 | 2561,1,2 1154 | 261,1,2 1155 | 2688,1.25,2 1156 | 2972,1,2 1157 | 3360,2,2 1158 | 534,3,2 1159 | 540,2,2 1160 | 597,2,2 1161 | 622,1,2 1162 | 667,2,2 1163 | 756,2,2 1164 | 760,1,2 1165 | 763,2,2 1166 | 767,1.25,2 1167 | 778,1,2 1168 | 785,2,2 1169 | 810,1,2 1170 | 845,1,2 1171 | 861,1,2 1172 | 868,1.25,2 1173 | 875,2,2 1174 | 896,1,2 1175 | 900,1,2 1176 | 901,1,2 1177 | 908,1,2 1178 | 917,1,2 1179 | 918,1,2 1180 | 948,1,2 1181 | 951,1,2 1182 | 952,2,2 1183 | 953,1,2 1184 | 954,1,2 1185 | 956,1,2 1186 | 962,1,2 1187 | 964,1,2 1188 | 969,1,2 1189 | 983,1,2 1190 | 988,2,2 1191 | 993,1,2 1192 | 998,1,2 1193 | 1000,2,1 1194 | 1006,1,1 1195 | 1006,2,1 1196 | 1008,2,1 1197 | 1010,1,1 1198 | 1011,1,1 1199 | 1011,2,1 1200 | 1013,1,1 1201 | 1015,1,1 1202 | 1017,1,1 1203 | 1018,1,1 1204 | 1018,2,1 1205 | 1019,1,1 1206 | 1020,2,1 1207 | 1022,2,1 1208 | 1023,1.25,1 1209 | 1023,2,1 1210 | 1025,2,1 1211 | 1026,1.25,1 1212 | 1028,1,1 1213 | 1028,2,1 1214 | 1029,1,1 1215 | 1029,2,1 1216 | 1032,1,1 1217 | 1035,1,1 1218 | 1035,2,1 1219 | 1036,2,1 1220 | 1037,1,1 1221 | 1038,2,1 1222 | 1039,2,1 1223 | 1041,2,1 1224 | 1045,1,1 1225 | 1046,2,1 1226 | 1047,2,1 1227 | 1049,1,1 1228 | 1050,1,1 1229 | 1050,2.5,1 1230 | 1051,2,1 1231 | 1052,2,1 1232 | 1053,1,1 1233 | 1054,1.25,1 1234 | 1059,2,1 1235 | 1061,2,1 1236 | 1063,1,1 1237 | 1065,1.5,1 1238 | 1066,1,1 1239 | 1067,1.5,1 1240 | 1067,2,1 1241 | 1067,2.4,1 1242 | 1068,1,1 1243 | 1071,1,1 1244 | 1071,1.5,1 1245 | 1071,2,1 1246 | 1072,2,1 1247 | 1073,1,1 1248 | 1074,2,1 1249 | 1079,2,1 1250 | 1080,1.25,1 1251 | 1081,1,1 1252 | 1082,1,1 1253 | 1084,1,1 1254 | 1084,2,1 1255 | 1087,2,1 1256 | 1088,2,1 1257 | 1090,1,1 1258 | 1090,2,1 1259 | 1091,2,1 1260 | 1094,1,1 1261 | 1095,2,1 1262 | 1097,1.75,1 1263 | 1098,1.75,1 1264 | 1102,1.25,1 1265 | 1102,2,1 1266 | 1103,1,1 1267 | 1104,1,1 1268 | 1107,2,1 1269 | 1108,1,1 1270 | 1109,2,1 1271 | 1111,2,1 1272 | 1115,1,1 1273 | 1117,1,1 1274 | 1117,2,1 1275 | 1118,1,1 1276 | 1119,1,1 1277 | 1119,2,1 1278 | 1120,1.25,1 1279 | 1123,1,1 1280 | 1127,1,1 1281 | 1127,2,1 1282 | 1128,2,1 1283 | 1131,1,1 1284 | 1131,2,1 1285 | 1132,2,1 1286 | 1133,1,1 1287 | 1134,1,1 1288 | 1136,1,1 1289 | 1137,2.5,1 1290 | 1138,2.25,1 1291 | 1139,1.2,1 1292 | 1142,1,1 1293 | 1143,2,1 1294 | 1144,2,1 1295 | 1147,1,1 1296 | 1148,1,1 1297 | 1149,1,1 1298 | 1149,2,1 1299 | 1150,1.091,1 1300 | 1151,1,1 1301 | 1151,1.25,1 1302 | 1152,2,1 1303 | 1152,2.222,1 1304 | 1154,1,1 1305 | 1156,2,1 1306 | 1159,1,1 1307 | 1159,2,1 1308 | 1162,1.25,1 1309 | 1163,1,1 1310 | 1163,2,1 1311 | 1165,2,1 1312 | 1166,1,1 1313 | 1166,2,1 1314 | 1167,1,1 1315 | 1168,1,1 1316 | 1168,2,1 1317 | 1170,1,1 1318 | 1171,1,1 1319 | 1172,2,1 1320 | 1174,1,1 1321 | 1174,1.25,1 1322 | 1174,2,1 1323 | 1176,1,1 1324 | 1176,1.25,1 1325 | 1177,2,1 1326 | 1178,1.364,1 1327 | 1178,2,1 1328 | 1179,1,1 1329 | 1182,2,1 1330 | 1184,1.622,1 1331 | 1185,1,1 1332 | 1188,1.5,1 1333 | 1189,1,1 1334 | 1189,2,1 1335 | 1191,1.5,1 1336 | 1193,1,1 1337 | 1193,2,1 1338 | 1194,2,1 1339 | 1195,2,1 1340 | 1197,1,1 1341 | 1201,2,1 1342 | 1204,2,1 1343 | 1205,2,1 1344 | 1206,1.5,1 1345 | 1207,2,1 1346 | 1208,1,1 1347 | 1211,1,1 1348 | 1212,2,1 1349 | 1213,2,1 1350 | 1216,3,1 1351 | 1219,1,1 1352 | 1220,1,1 1353 | 1221,1,1 1354 | 1222,1,1 1355 | 1222,1.5,1 1356 | 1222,2,1 1357 | 1223,1,1 1358 | 1224,1,1 1359 | 1224,1.5,1 1360 | 1224,2,1 1361 | 1228,1.5,1 1362 | 1228,2,1 1363 | 1229,1,1 1364 | 1229,2,1 1365 | 1231,1,1 1366 | 1236,1,1 1367 | 1236,1.25,1 1368 | 1237,2,1 1369 | 1238,1.104,1 1370 | 1238,2,1 1371 | 1242,2,1 1372 | 1243,2,1 1373 | 1244,2,1 1374 | 1246,1.9,1 1375 | 1248,1,1 1376 | 1248,1.538,1 1377 | 1249,1,1 1378 | 1249,2,1 1379 | 1251,2,1 1380 | 1252,1.091,1 1381 | 1256,1.5,1 1382 | 1260,2.5,1 1383 | 1261,2,1 1384 | 1264,1.1,1 1385 | 1265,1.5,1 1386 | 1265,2,1 1387 | 1266,1.5,1 1388 | 1267,1.5,1 1389 | 1271,1,1 1390 | 1271,1.5,1 1391 | 1271,2,1 1392 | 1272,1.5,1 1393 | 1277,0.896,1 1394 | 1281,1.2,1 1395 | 1287,2,1 1396 | 1288,2,1 1397 | 1289,2,1 1398 | 1297,1,1 1399 | 1299,2,1 1400 | 1300,1,1 1401 | 1304,2,1 1402 | 1305,1,1 1403 | 1305,2,1 1404 | 1306,2,1 1405 | 1309,2.2,1 1406 | 1312,2,1 1407 | 1313,1.2,1 1408 | 1315,2,1 1409 | 1316,2,1 1410 | 1321,2,1 1411 | 1325,1,1 1412 | 1325,2,1 1413 | 1327,2,1 1414 | 1328,2,1 1415 | 1329,1.25,1 1416 | 1329,1.375,1 1417 | 1332,2.25,1 1418 | 1333,1.2,1 1419 | 1337,2,1 1420 | 1338,1.2,1 1421 | 1339,2,1 1422 | 1340,2,1 1423 | 1342,2,1 1424 | 1343,1,1 1425 | 1343,1.25,1 1426 | 1343,2,1 1427 | 1344,1.429,1 1428 | 1346,1.25,1 1429 | 1346,2,1 1430 | 1348,2,1 1431 | 1349,1,1 1432 | 1349,2,1 1433 | 1350,2,1 1434 | 1351,1,1 1435 | 1352,2,1 1436 | 1355,2,1 1437 | 1356,2,1 1438 | 1363,1.395,1 1439 | 1367,2,1 1440 | 1370,2,1 1441 | 1373,1.4,1 1442 | 1375,2,1 1443 | 1376,1,1 1444 | 1376,1.395,1 1445 | 1376,2,1 1446 | 1377,1,1 1447 | 1382,1.25,1 1448 | 1382,1.333,1 1449 | 1384,1,1 1450 | 1388,1,1 1451 | 1394,2,1 1452 | 1396,1.375,1 1453 | 1398,2,1 1454 | 1402,1,1 1455 | 1402,2,1 1456 | 1406,1,1 1457 | 1406,2,1 1458 | 1411,1,1 1459 | 1411,1.7,1 1460 | 1413,1.1,1 1461 | 1413,1.8,1 1462 | 1414,2,1 1463 | 1416,1.25,1 1464 | 1419,2,1 1465 | 1420,1,1 1466 | 1420,2,1 1467 | 1421,2.25,1 1468 | 1422,1.091,1 1469 | 1422,1.35,1 1470 | 1423,1.8,1 1471 | 1424,2,1 1472 | 1425,1,1 1473 | 1426,1.25,1 1474 | 1426,2,1 1475 | 1427,2.25,1 1476 | 1431,1,1 1477 | 1432,2,1 1478 | 1433,2,1 1479 | 1434,1.25,1 1480 | 1436,1.25,1 1481 | 1443,1,1 1482 | 1443,2,1 1483 | 1445,2,1 1484 | 1446,1,1 1485 | 1447,1,1 1486 | 1448,1,1 1487 | 1448,1.25,1 1488 | 1449,1,1 1489 | 1450,1,1 1490 | 1452,2,1 1491 | 1453,1,1 1492 | 1454,2,1 1493 | 1455,1.1,1 1494 | 1457,2,1 1495 | 1460,1.25,1 1496 | 1461,2,1 1497 | 1463,1.75,1 1498 | 1464,1,1 1499 | 1464,2,1 1500 | 1465,2,1 1501 | 1466,1,1 1502 | 1468,2,1 1503 | 1469,1,1 1504 | 1470,1.25,1 1505 | 1470,1.3,1 1506 | 1471,1,1 1507 | 1471,2,1 1508 | 1473,1,1 1509 | 1473,2,1 1510 | 1474,2,1 1511 | 1475,1.3,1 1512 | 1476,2,1 1513 | 1477,1.3,1 1514 | 1477,2,1 1515 | 1478,2,1 1516 | 1479,1,1 1517 | 1480,2,1 1518 | 1482,2,1 1519 | 1483,1,1 1520 | 1483,2,1 1521 | 1486,2,1 1522 | 1488,1.25,1 1523 | 1489,1,1 1524 | 1490,1.25,1 1525 | 1490,1.5,1 1526 | 1491,1,1 1527 | 1495,2,1 1528 | 1496,1.25,1 1529 | 1497,1,1 1530 | 1502,1.25,1 1531 | 1503,1.25,1 1532 | 1505,1.818,1 1533 | 1506,2,1 1534 | 1508,1,1 1535 | 1508,2,1 1536 | 1509,1,1 1537 | 1509,2.2,1 1538 | 1510,1,1 1539 | 1511,1.125,1 1540 | 1513,1,1 1541 | 1519,0.896,1 1542 | 1519,1.25,1 1543 | 1520,1,1 1544 | 1520,1.25,1 1545 | 1520,2,1 1546 | 1524,1,1 1547 | 1525,1.5,1 1548 | 1526,1,1 1549 | 1526,2.5,1 1550 | 1528,2.2,1 1551 | 1529,1.256,1 1552 | 1530,2,1 1553 | 1531,2,1 1554 | 1533,2,1 1555 | 1534,2,1 1556 | 1535,2.5,1 1557 | 1537,2,1 1558 | 1539,1,1 1559 | 1540,2.5,1 1560 | 1543,1,1 1561 | 1544,1,1 1562 | 1545,2,1 1563 | 1549,2,1 1564 | 1550,1.765,1 1565 | 1552,2,1 1566 | 1553,2,1 1567 | 1556,2,1 1568 | 1557,1,1 1569 | 1557,2,1 1570 | 1560,1,1 1571 | 1566,1,1 1572 | 1567,1,1 1573 | 1568,2,1 1574 | 1570,1,1 1575 | 1574,1.5,1 1576 | 1575,2,1 1577 | 1577,1,1 1578 | 1577,2,1 1579 | 1578,1.8,1 1580 | 1579,1,1 1581 | 1579,2,1 1582 | 1580,1,1 1583 | 1580,2,1 1584 | 1581,1,1 1585 | 1581,2,1 1586 | 1584,1.818,1 1587 | 1589,1,1 1588 | 1590,2,1 1589 | 1593,2,1 1590 | 1596,2,1 1591 | 1597,1.2,1 1592 | 1597,2,1 1593 | 1598,2,1 1594 | 1599,1,1 1595 | 1600,1.2,1 1596 | 1600,1.6,1 1597 | 1601,2,1 1598 | 1602,2,1 1599 | 1604,2,1 1600 | 1605,2,1 1601 | 1606,2,1 1602 | 1607,1,1 1603 | 1607,2,1 1604 | 1608,1,1 1605 | 1608,2,1 1606 | 1609,2,1 1607 | 1617,2,1 1608 | 1618,2,1 1609 | 1619,1,1 1610 | 1621,2,1 1611 | 1624,2,1 1612 | 1628,1,1 1613 | 1629,2,1 1614 | 1632,1,1 1615 | 1634,1,1 1616 | 1636,1,1 1617 | 1636,1.091,1 1618 | 1637,2,1 1619 | 1639,1,1 1620 | 1639,2.5,1 1621 | 1641,1,1 1622 | 1645,2,1 1623 | 1646,1,1 1624 | 1647,1,1 1625 | 1649,2,1 1626 | 1650,1,1 1627 | 1652,1,1 1628 | 1652,2,1 1629 | 1653,2,1 1630 | 1654,1,1 1631 | 1655,1,1 1632 | 1656,1,1 1633 | 1659,1,1 1634 | 1662,1,1 1635 | 1663,1,1 1636 | 1663,2,1 1637 | 1666,2,1 1638 | 1667,1,1 1639 | 1667,2,1 1640 | 1668,2.222,1 1641 | 1669,1,1 1642 | 1674,1,1 1643 | 1674,2,1 1644 | 1675,1,1 1645 | 1675,2,1 1646 | 1683,1,1 1647 | 1684,0.95,1 1648 | 1684,2,1 1649 | 1685,1,1 1650 | 1685,1.1,1 1651 | 1686,2,1 1652 | 1687,1,1 1653 | 1688,1,1 1654 | 1689,1,1 1655 | 1689,1.1,1 1656 | 1696,2,1 1657 | 1697,1,1 1658 | 1700,1,1 1659 | 1704,0.9,1 1660 | 1704,2,1 1661 | 1712,1,1 1662 | 1712,2.25,1 1663 | 1713,2,1 1664 | 1719,2,1 1665 | 1723,2,1 1666 | 1725,1,1 1667 | 1729,1,1 1668 | 1730,1,1 1669 | 1731,1.1,1 1670 | 1732,1,1 1671 | 1734,1,1 1672 | 1737,0.75,1 1673 | 1739,2,1 1674 | 1741,2,1 1675 | 1743,2,1 1676 | 1744,1,1 1677 | 1745,1,1 1678 | 1746,2,1 1679 | 1748,1,1 1680 | 1749,2,1 1681 | 1750,1,1 1682 | 1750,2,1 1683 | 1752,1,1 1684 | 1754,1,1 1685 | 1760,1.091,1 1686 | 1761,2,1 1687 | 1762,2,1 1688 | 1765,2,1 1689 | 1767,2,1 1690 | 1770,0.896,1 1691 | 1770,1,1 1692 | 1771,1,1 1693 | 1771,2,1 1694 | 1772,1,1 1695 | 1774,1,1 1696 | 1774,1.25,1 1697 | 1774,2,1 1698 | 1775,2,1 1699 | 1784,2,1 1700 | 1787,2,1 1701 | 1788,1,1 1702 | 1790,2,1 1703 | 1792,2,1 1704 | 1793,2,1 1705 | 1794,2,1 1706 | 1795,2,1 1707 | 1796,2,1 1708 | 1798,1,1 1709 | 1798,2,1 1710 | 1799,1,1 1711 | 1799,2,1 1712 | 1800,2,1 1713 | 1802,1.818,1 1714 | 1804,1,1 1715 | 1805,2,1 1716 | 1806,1.25,1 1717 | 1811,2,1 1718 | 1812,1,1 1719 | 1813,0.75,1 1720 | 1813,2,1 1721 | 1816,1.057,1 1722 | 1821,2,1 1723 | 1822,2,1 1724 | 1823,1,1 1725 | 1829,2,1 1726 | 1831,1,1 1727 | 1832,2,1 1728 | 1834,1,1 1729 | 1835,1,1 1730 | 1836,2,1 1731 | 1837,1,1 1732 | 1838,1,1 1733 | 1838,2,1 1734 | 1839,2,1 1735 | 1844,2,1 1736 | 1846,2,1 1737 | 1847,2,1 1738 | 1848,2,1 1739 | 1851,2,1 1740 | 1856,2,1 1741 | 1859 px,2 dppx,1 1742 | 1861,2,1 1743 | 1868,1,1 1744 | 1868,2,1 1745 | 1870,1,1 1746 | 1870,2,1 1747 | 1872,2,1 1748 | 1874,1,1 1749 | 1875,2,1 1750 | 1878,2,1 1751 | 1881,2,1 1752 | 1882,0.85,1 1753 | 1883,1,1 1754 | 1887,1,1 1755 | 1890,1,1 1756 | 1892,2,1 1757 | 1895,1,1 1758 | 1895,2,1 1759 | 1898,2,1 1760 | 1901,2,1 1761 | 1902,0.896,1 1762 | 1903,2,1 1763 | 1904,1.25,1 1764 | 1904,2,1 1765 | 1907,1,1 1766 | 1907,2,1 1767 | 1913,2,1 1768 | 1915,2,1 1769 | 1918,1.5,1 1770 | 1918,2,1 1771 | 1920,1.333,1 1772 | 1920,1.5,1 1773 | 1921,2,1 1774 | 1922,2,1 1775 | 1923,1,1 1776 | 1923,1.25,1 1777 | 1925,1,1 1778 | 1928,2,1 1779 | 1929,2,1 1780 | 1933,1,1 1781 | 1936,2,1 1782 | 1940,2,1 1783 | 1942,1,1 1784 | 1944,1,1 1785 | 1951,1,1 1786 | 1953,0.896,1 1787 | 1953,1,1 1788 | 1956,1,1 1789 | 1956,2,1 1790 | 1958,2,1 1791 | 1969,1,1 1792 | 1974,1,1 1793 | 1976,2,1 1794 | 1977,1,1 1795 | 1979,2,1 1796 | 1980,2,1 1797 | 1981,1,1 1798 | 1983,2,1 1799 | 1984,1,1 1800 | 1985,1,1 1801 | 1986,2,1 1802 | 1989,1,1 1803 | 1989,2,1 1804 | 1990,2,1 1805 | 1996,2,1 1806 | 2000,0.8,1 1807 | 2000,1.5,1 1808 | 2001,1,1 1809 | 2005,1,1 1810 | 2005,2,1 1811 | 2007,2,1 1812 | 2009,1.5,1 1813 | 2010,2,1 1814 | 2012,2,1 1815 | 2013,1,1 1816 | 2013,2,1 1817 | 2015,1,1 1818 | 2017,1,1 1819 | 2019,2,1 1820 | 2021,1,1 1821 | 2024,1,1 1822 | 2029,2,1 1823 | 2030,2,1 1824 | 2032,1,1 1825 | 2033,1,1 1826 | 2034,2,1 1827 | 2035,2,1 1828 | 2037,2,1 1829 | 2038,1.25,1 1830 | 2039,1,1 1831 | 2040,1,1 1832 | 2041,2,1 1833 | 2042,1,1 1834 | 2045,1,1 1835 | 2046,1.25,1 1836 | 2049,2,1 1837 | 2050,1,1 1838 | 2052,2,1 1839 | 2056,1,1 1840 | 2056,2,1 1841 | 2059,1,1 1842 | 2061,1,1 1843 | 2065,1,1 1844 | 2067,1,1 1845 | 2068,1,1 1846 | 2070,1,1 1847 | 2073,2,1 1848 | 2077,2,1 1849 | 2078,1,1 1850 | 2079,2,1 1851 | 2085,1,1 1852 | 2088,1,1 1853 | 2089,1,1 1854 | 2090,2,1 1855 | 2094,2,1 1856 | 2095,1,1 1857 | 2096,2,1 1858 | 2097,2,1 1859 | 2100,1,1 1860 | 2101,1,1 1861 | 2105,2,1 1862 | 2112,1.818,1 1863 | 2115,2,1 1864 | 2116,1,1 1865 | 2117,1,1 1866 | 2122,1,1 1867 | 2123,1,1 1868 | 2128,1,1 1869 | 2133,1,1 1870 | 2134,1,1 1871 | 2134,1.5,1 1872 | 2135,1,1 1873 | 2139,1,1 1874 | 2146,1,1 1875 | 2149,1,1 1876 | 2149,2,1 1877 | 2150,1,1 1878 | 2151,1,1 1879 | 2154,1,1 1880 | 2155,1,1 1881 | 2158,1,1 1882 | 2158,2,1 1883 | 2160,1.5,1 1884 | 2171,1,1 1885 | 2173,1.76,1 1886 | 2175,1,1 1887 | 2175,2,1 1888 | 2176,1.765,1 1889 | 2177,1,1 1890 | 2186,2,1 1891 | 2188,1,1 1892 | 2192,1.75,1 1893 | 2194,1.75,1 1894 | 2200,1,1 1895 | 2201,2,1 1896 | 2204,1,1 1897 | 2207,1,1 1898 | 2210,1,1 1899 | 2211,2,1 1900 | 2212,2,1 1901 | 2213,1,1 1902 | 2215,1,1 1903 | 2218,1,1 1904 | 2221,2,1 1905 | 2224,1,1 1906 | 2224,2,1 1907 | 2228,1,1 1908 | 2231,2,1 1909 | 2233,1,1 1910 | 2236,1,1 1911 | 2244,1,1 1912 | 2251,1,1 1913 | 2252,1,1 1914 | 2256,1,1 1915 | 2258,1,1 1916 | 2259,0.85,1 1917 | 2269,1,1 1918 | 2272,1,1 1919 | 2274,1.1,1 1920 | 2282,1,1 1921 | 2285,1,1 1922 | 2287,1,1 1923 | 2289,1,1 1924 | 2290,1,1 1925 | 2293,1,1 1926 | 2294,1.5,1 1927 | 2304,1.25,1 1928 | 2305,1,1 1929 | 2308,2,1 1930 | 2311,1,1 1931 | 2314,2,1 1932 | 2316,1,1 1933 | 2319,1,1 1934 | 2319,2,1 1935 | 2322,1,1 1936 | 2325,1,1 1937 | 2330,1,1 1938 | 2335,1,1 1939 | 2337,1,1 1940 | 2341,1,1 1941 | 2343,1,1 1942 | 2346,1,1 1943 | 2347,1.091,1 1944 | 2357,1,1 1945 | 2359,1,1 1946 | 2360,2,1 1947 | 2364,1,1 1948 | 2365,2,1 1949 | 2370,1,1 1950 | 2371,2,1 1951 | 2373,2,1 1952 | 2375,1.25,1 1953 | 2376,1,1 1954 | 2380,1,1 1955 | 2385,2,1 1956 | 2389,1,1 1957 | 2391,2,1 1958 | 2394,1,1 1959 | 2398,1,1 1960 | 2400,1,1 1961 | 2402,1,1 1962 | 2403,1,1 1963 | 2408,1,1 1964 | 2410,2,1 1965 | 2418,1,1 1966 | 2430,1,1 1967 | 2433,1,1 1968 | 2433,2,1 1969 | 2441,1,1 1970 | 2447,1,1 1971 | 2458,1,1 1972 | 2461,1,1 1973 | 2463,1,1 1974 | 2470,1,1 1975 | 2470,2,1 1976 | 2473,2,1 1977 | 2481,1,1 1978 | 2485,1,1 1979 | 2492,1,1 1980 | 2504,1,1 1981 | 2506,1,1 1982 | 2508,1,1 1983 | 2510,1,1 1984 | 2512,2,1 1985 | 2513,1,1 1986 | 2513,2,1 1987 | 2514,1,1 1988 | 2525,2,1 1989 | 2528,2,1 1990 | 2534,2,1 1991 | 2536,1,1 1992 | 2538,1,1 1993 | 2539,1,1 1994 | 2539,2,1 1995 | 2541,1.5,1 1996 | 2545,2,1 1997 | 2548,1,1 1998 | 2549,1,1 1999 | 2551,1,1 2000 | 2552,2,1 2001 | 2553,1,1 2002 | 2554,1,1 2003 | 2555,2,1 2004 | 2556,2,1 2005 | 2559,1.5,1 2006 | 2563,1.5,1 2007 | 2564,1,1 2008 | 2564,2,1 2009 | 2566,1.5,1 2010 | 2579,2,1 2011 | 2597,1,1 2012 | 2599,2,1 2013 | 2609,1,1 2014 | 2621,1,1 2015 | 2669,2,1 2016 | 2677,1,1 2017 | 2686,2,1 2018 | 2704,2,1 2019 | 2713,1,1 2020 | 2717,1,1 2021 | 2721,1,1 2022 | 2727,1,1 2023 | 2738,1,1 2024 | 2811,1,1 2025 | 2831,2,1 2026 | 2859,0.896,1 2027 | 2891,1,1 2028 | 2943,2,1 2029 | 2955,1,1 2030 | 2966,2,1 2031 | 300,2,1 2032 | 3072,1.25,1 2033 | 3200,2,1 2034 | 3323,1.156,1 2035 | 3359,2,1 2036 | 3377,1,1 2037 | 3424,1,1 2038 | 346,3.125,1 2039 | 361,1.331,1 2040 | 3773,1,1 2041 | 3810,0.667,1 2042 | 3839,1,1 2043 | 396,2.727,1 2044 | 400,1,1 2045 | 400,2.7,1 2046 | 4072,1,1 2047 | 412,1,1 2048 | 438,3,1 2049 | 470,2,1 2050 | 480,1.5,1 2051 | 484,1,1 2052 | 523,1,1 2053 | 559,2,1 2054 | 562,1,1 2055 | 576,2,1 2056 | 581,1,1 2057 | 592,2,1 2058 | 599,2,1 2059 | 600,1,1 2060 | 601,2,1 2061 | 614,2,1 2062 | 638,2,1 2063 | 673,2,1 2064 | 692,2,1 2065 | 694,2,1 2066 | 698,2,1 2067 | 699,1,1 2068 | 701,2,1 2069 | 716,2,1 2070 | 717,2,1 2071 | 720,1,1 2072 | 723,2,1 2073 | 728,1,1 2074 | 731,1,1 2075 | 732,2.625,1 2076 | 736,3,1 2077 | 743,2,1 2078 | 745,1,1 2079 | 751,1,1 2080 | 752,1,1 2081 | 758,1,1 2082 | 761,1,1 2083 | 768,1,1 2084 | 768,1.25,1 2085 | 769,1,1 2086 | 771,1,1 2087 | 776,1.25,1 2088 | 779,2,1 2089 | 784,1,1 2090 | 789,1,1 2091 | 793,1,1 2092 | 794,1,1 2093 | 799,1,1 2094 | 802,1,1 2095 | 815,2,1 2096 | 816,1,1 2097 | 818,1,1 2098 | 820,1,1 2099 | 822,1,1 2100 | 823,1,1 2101 | 823,2.5,1 2102 | 834,1,1 2103 | 836,1,1 2104 | 838,1,1 2105 | 838,2,1 2106 | 840,1,1 2107 | 850,1.5,1 2108 | 852,1.5,1 2109 | 855,1,1 2110 | 856,1,1 2111 | 856,2,1 2112 | 857,2,1 2113 | 859,2,1 2114 | 860,2,1 2115 | 862,2,1 2116 | 863,2,1 2117 | 866,2,1 2118 | 867,1,1 2119 | 869,1,1 2120 | 873,1,1 2121 | 877,1,1 2122 | 880,2,1 2123 | 881,2,1 2124 | 885,1,1 2125 | 887,1,1 2126 | 888,2,1 2127 | 890,1,1 2128 | 891,1,1 2129 | 893,2,1 2130 | 896,2,1 2131 | 898,2,1 2132 | 905,1,1 2133 | 906,1,1 2134 | 906,2,1 2135 | 907,1,1 2136 | 909,1,1 2137 | 912,1,1 2138 | 912,1.25,1 2139 | 915,1,1 2140 | 919,2,1 2141 | 919 px,2 dppx,1 2142 | 920,1,1 2143 | 921,2,1 2144 | 922,2,1 2145 | 923,2,1 2146 | 925,1,1 2147 | 926,1,1 2148 | 926,1.75,1 2149 | 926,2,1 2150 | 928,1.25,1 2151 | 933,2,1 2152 | 934,1,1 2153 | 934,2,1 2154 | 935,1,1 2155 | 937,1.375,1 2156 | 939,1,1 2157 | 941,1,1 2158 | 941,2,1 2159 | 942,1,1 2160 | 950,2,1 2161 | 953,2,1 2162 | 954,2,1 2163 | 956,1.25,1 2164 | 956,2,1 2165 | 958,2,1 2166 | 959,2,1 2167 | 962,1.331,1 2168 | 962,2,1 2169 | 963,1,1 2170 | 964,2,1 2171 | 965,1,1 2172 | 966,1,1 2173 | 970,2,1 2174 | 971,1,1 2175 | 971,2,1 2176 | 971,3,1 2177 | 972,1,1 2178 | 977,2,1 2179 | 979,1,1 2180 | 980,2,1 2181 | 980,3,1 2182 | 980,4,1 2183 | 981,1,1 2184 | 981,1.5,1 2185 | 981,2,1 2186 | 981,2.625,1 2187 | 982,1,1 2188 | 982,2,1 2189 | 986,1,1 2190 | 986,1.25,1 2191 | 989,2,1 2192 | 990,2,1 2193 | 994,2,1 2194 | -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/webpagetest-resilient-web-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleverage/daltons/54df2bb52f6949ffc70cf4b629393c76644ed285/examples/nicolas-hoizey.com/webpagetest-resilient-web-design.png -------------------------------------------------------------------------------- /examples/nicolas-hoizey.com/what-does-my-site-cost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cleverage/daltons/54df2bb52f6949ffc70cf4b629393c76644ed285/examples/nicolas-hoizey.com/what-does-my-site-cost.png -------------------------------------------------------------------------------- /examples/simple/configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | statsFile: `${__dirname}/stats.csv`, 3 | url: `file://${__dirname}/page.html`, 4 | selector: 'img', 5 | delay: 100, 6 | widthsNumber: 3, 7 | } 8 | -------------------------------------------------------------------------------- /examples/simple/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple image 5 | 6 | 7 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/simple/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | path=$(pwd) 3 | node ../../src/cli.js --stats-file ./stats.csv --url "file://${path}/page.html" --selector 'img' --verbose 4 | -------------------------------------------------------------------------------- /examples/simple/stats.csv: -------------------------------------------------------------------------------- 1 | viewport width (px),screen density (dppx),page views 2 | 1020,1,35 3 | 1021,2,3 4 | 1022,1,53 5 | 1023,2,12 6 | 1024,1,23 7 | 1025,1.5,27 8 | 1026,1,28 9 | 1027,1.25,32 10 | 1028,1,7 11 | 1029,1,18 12 | 1030,1.1,23 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daltons", 3 | "version": "0.0.1", 4 | "author": "Nicolas Hoizey ", 5 | "description": "A tool helping the choice of optimal responsive image sizes", 6 | "license": "MIT", 7 | "repository": { 8 | "url": "https://github.com/cleverage/daltons/" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/cleverage/daltons/issues" 12 | }, 13 | "engines": { 14 | "node": ">=8" 15 | }, 16 | "main": "./src/index.js", 17 | "bin": { 18 | "daltons": "./src/cli.js" 19 | }, 20 | "scripts": { 21 | "test": "npm run test:lint && npm run test:examples", 22 | "test:lint": "npx eslint .", 23 | "test:examples": "jest" 24 | }, 25 | "dependencies": { 26 | "ansi-colors": "^4.1.1", 27 | "cli-table": "^0.3.11", 28 | "csv-parse": "^4.16.0", 29 | "puppeteer": "^13.7.0", 30 | "yargs": "^17.4.1" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^8.14.0", 34 | "eslint-config-prettier": "^8.5.0", 35 | "eslint-plugin-prettier": "^4.0.0", 36 | "jest": "^28.0.3", 37 | "prettier": "^2.6.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | semi: false, 5 | } 6 | -------------------------------------------------------------------------------- /src/adjustDensitiesAndViewportsWithStats.js: -------------------------------------------------------------------------------- 1 | const color = require('ansi-colors') 2 | const logger = require('./logger') 3 | 4 | module.exports = function adjustDensitiesAndViewportsWithStats(stats, opt) { 5 | let result = {} 6 | 7 | // check screen densities 8 | const statsMinDensity = stats.reduce( 9 | (min, p) => (p.density < min ? p.density : min), 10 | stats[0].density, 11 | ) 12 | const statsMaxDensity = stats.reduce( 13 | (max, p) => (p.density > max ? p.density : max), 14 | stats[0].density, 15 | ) 16 | logger.info( 17 | `\nScreen densities in stats go from ${color.green( 18 | statsMinDensity, 19 | )} to ${color.green(statsMaxDensity)}`, 20 | ) 21 | result.minDensity = statsMinDensity 22 | result.maxDensity = statsMaxDensity 23 | 24 | if (opt.minDensity || opt.maxDensity) { 25 | if (opt.minDensity) { 26 | result.minDensity = Math.max(statsMinDensity, opt.minDensity) 27 | } 28 | if (opt.maxDensity) { 29 | result.maxDensity = Math.min(statsMaxDensity, opt.maxDensity) 30 | } 31 | logger.info( 32 | `Screen densities will be limited from ${color.green( 33 | result.minDensity, 34 | )} to ${color.green(result.maxDensity)}`, 35 | ) 36 | } 37 | 38 | // check viewports 39 | const statsMinViewport = stats.reduce( 40 | (min, p) => (p.viewport < min ? p.viewport : min), 41 | stats[0].viewport, 42 | ) 43 | const statsMaxViewport = stats.reduce( 44 | (max, p) => (p.viewport > max ? p.viewport : max), 45 | stats[0].viewport, 46 | ) 47 | logger.info( 48 | `\nViewports in stats go from ${color.green( 49 | statsMinViewport + 'px', 50 | )} to ${color.green(statsMaxViewport + 'px')}`, 51 | ) 52 | result.minViewport = statsMinViewport 53 | result.maxViewport = statsMaxViewport 54 | 55 | if (opt.minViewport || opt.maxViewport) { 56 | if (opt.minViewport) { 57 | result.minViewport = Math.max(statsMinViewport, opt.minViewport) 58 | } 59 | if (opt.maxViewport) { 60 | result.maxViewport = Math.min(statsMaxViewport, opt.maxViewport) 61 | } 62 | logger.info( 63 | `Viewports will be limited from ${color.green( 64 | result.minViewport + 'px', 65 | )} to ${color.green(result.maxViewport + 'px')}`, 66 | ) 67 | } 68 | 69 | return result 70 | } 71 | -------------------------------------------------------------------------------- /src/browse.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | const path = require('path') 4 | const puppeteer = require('puppeteer') 5 | const color = require('ansi-colors') 6 | const logger = require('./logger') 7 | 8 | const writeFile = util.promisify(fs.writeFile) 9 | 10 | const sleep = (timeout) => new Promise((r) => setTimeout(r, timeout)) 11 | 12 | module.exports = async function browse(opt) { 13 | const VIEWPORT = { 14 | width: opt.minViewport, 15 | height: 2000, 16 | deviceScaleFactor: 1, 17 | } 18 | const imageWidths = new Map() 19 | 20 | logger.info('Launch headless Chrome') 21 | 22 | const browser = await puppeteer.launch() 23 | const page = await browser.newPage() 24 | 25 | logger.info(`Navigate to ${color.green(opt.url)}`) 26 | 27 | await page 28 | .goto(opt.url, { waitUntil: 'networkidle2' }) 29 | .then(async () => { 30 | logger.info(`Check widths of image ${color.green(opt.selector)}`) 31 | 32 | const spinner = logger.newSpinner() 33 | if (spinner) { 34 | spinner.start('Starting…') 35 | } 36 | 37 | while (VIEWPORT.width <= opt.maxViewport) { 38 | // Update log in the console 39 | if (spinner) { 40 | spinner.tick( 41 | `Current viewport: ${color.green(VIEWPORT.width + 'px')}`, 42 | ) 43 | } 44 | // Set new viewport width 45 | await page.setViewport(VIEWPORT) 46 | 47 | // Give the browser some time to adjust layout, sometimes requiring JS 48 | await sleep(opt.delay) 49 | 50 | // Check image width 51 | await page.waitForSelector(opt.selector) 52 | let imageWidth = await page.evaluate((sel) => { 53 | return document.querySelector(sel).width 54 | }, opt.selector) 55 | imageWidths.set(VIEWPORT.width, imageWidth) 56 | 57 | // Increment viewport width 58 | VIEWPORT.width++ 59 | } 60 | 61 | if (spinner) { 62 | spinner.stop(`Finished at viewport: ${color.green(opt.maxViewport)}px`) 63 | } 64 | 65 | // Save data into the CSV file 66 | if (opt.variationsFile) { 67 | let csvString = 'viewport width (px);image width (px)\n' 68 | imageWidths.forEach( 69 | (imageWidth, viewportWidth) => 70 | (csvString += `${viewportWidth};${imageWidth}` + '\n'), 71 | ) 72 | await writeFile( 73 | path.resolve(opt.basePath, opt.variationsFile), 74 | csvString, 75 | ) 76 | .then(() => { 77 | logger.info( 78 | color.green( 79 | `Image width variations saved to CSV file ${opt.variationsFile}`, 80 | ), 81 | ) 82 | }) 83 | .catch((error) => 84 | logger.error( 85 | `Couldn’t save image width variations to CSV file ${opt.variationsFile}:\n${error}`, 86 | ), 87 | ) 88 | } 89 | 90 | // Output clean table to the console 91 | const imageWidthsTable = logger.newTable({ 92 | head: ['viewport width', 'image width'], 93 | colAligns: ['right', 'right'], 94 | style: { 95 | head: ['green', 'green'], 96 | compact: true, 97 | }, 98 | }) 99 | if (imageWidthsTable) { 100 | imageWidths.forEach((imageWidth, viewportWidth) => 101 | imageWidthsTable.push([viewportWidth + 'px', imageWidth + 'px']), 102 | ) 103 | logger.info(imageWidthsTable.toString()) 104 | } 105 | }) 106 | .catch((error) => 107 | logger.error(`Couldn’t load page located at ${opt.url}:\n${error}`), 108 | ) 109 | 110 | await page.browser().close() 111 | 112 | return imageWidths 113 | } 114 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Choose optimal responsive image widths to put in your `srcset` attribute 4 | * 5 | * Usage: 6 | * 7 | * npx daltons -h 8 | */ 9 | const fs = require('fs') 10 | const path = require('path') 11 | const yargs = require('yargs') 12 | const color = require('ansi-colors') 13 | const run = require('./index.js') 14 | 15 | const currentPath = process.cwd() 16 | 17 | const argv = yargs 18 | .options({ 19 | statsFile: { 20 | alias: 'c', 21 | describe: 22 | 'File path from which reading the actual stats in CSV format (screen density in dppx, viewport width in px, number of page views)', 23 | demandOption: true, 24 | type: 'string', 25 | }, 26 | minViewport: { 27 | alias: 'i', 28 | describe: 'Minimum viewport width to check', 29 | type: 'number', 30 | }, 31 | maxViewport: { 32 | alias: 'x', 33 | describe: 'Maximum viewport width to check', 34 | type: 'number', 35 | }, 36 | minDensity: { 37 | describe: 'Minimum screen density to consider', 38 | type: 'number', 39 | }, 40 | maxDensity: { 41 | describe: 'Maximum screen density to consider', 42 | type: 'number', 43 | }, 44 | url: { 45 | alias: 'u', 46 | describe: 'Page URL', 47 | demandOption: true, 48 | }, 49 | selector: { 50 | alias: 's', 51 | describe: 'Image selector in the page', 52 | demandOption: true, 53 | }, 54 | delay: { 55 | alias: 'd', 56 | describe: 57 | 'Delay after viewport resizing before checking image width (ms)', 58 | default: 100, 59 | type: 'number', 60 | }, 61 | variationsFile: { 62 | alias: 'a', 63 | describe: 64 | 'File path to which saving the image width variations data, in CSV format', 65 | type: 'string', 66 | }, 67 | minPercentage: { 68 | describe: 69 | 'Minimum percentage of "perfect" image width to keep from stats (0 <= % < 1)', 70 | type: 'number', 71 | default: 0.0001, 72 | }, 73 | widthsDivisor: { 74 | alias: 'o', 75 | describe: 'Number by which the computed widths must be divisible', 76 | default: 10, 77 | type: 'number', 78 | }, 79 | widthsNumber: { 80 | alias: 'n', 81 | describe: 'Number of widths to recommend', 82 | default: 5, 83 | type: 'number', 84 | }, 85 | destFile: { 86 | alias: 'f', 87 | describe: 88 | 'File path to which saving the image widths for the srcset attribute', 89 | type: 'string', 90 | }, 91 | verbose: { 92 | alias: 'v', 93 | describe: 'Log progress and result in the console', 94 | }, 95 | }) 96 | .group( 97 | ['minViewport', 'maxViewport'], 98 | 'Global: limit viewport widths, for example for Art Direction (see docs)', 99 | ) 100 | .group( 101 | ['statsFile', 'minDensity', 'maxDensity'], 102 | 'Step 1: get actual stats of site visitors', 103 | ) 104 | .group( 105 | ['url', 'selector', 'delay', 'variationsFile'], 106 | 'Step 2: get variations of image width across viewport widths', 107 | ) 108 | .group( 109 | ['minPercentage', 'widthsDivisor', 'widthsNumber', 'destFile'], 110 | 'Step 3: compute optimal n widths from both datasets', 111 | ) 112 | .check((argv) => { 113 | // waiting for https://github.com/yargs/yargs/issues/1079 114 | if (argv.minViewport !== undefined && isNaN(argv.minViewport)) { 115 | throw new Error( 116 | color.red(`Error: ${color.redBright('minViewport')} must be a number`), 117 | ) 118 | } 119 | if (argv.minViewport < 0) { 120 | throw new Error( 121 | color.red(`Error: ${color.redBright('minViewport')} must be >= 0`), 122 | ) 123 | } 124 | if (argv.maxViewport !== undefined && isNaN(argv.maxViewport)) { 125 | throw new Error( 126 | color.red(`Error: ${color.redBright('maxViewport')} must be a number`), 127 | ) 128 | } 129 | if (argv.maxViewport < argv.minViewport) { 130 | throw new Error( 131 | color.red( 132 | `Error: ${color.redBright( 133 | 'maxViewport', 134 | )} must be greater than minViewport`, 135 | ), 136 | ) 137 | } 138 | if (argv.minDensity !== undefined && isNaN(argv.minDensity)) { 139 | throw new Error( 140 | color.red(`Error: ${color.redBright('minDensity')} must be a number`), 141 | ) 142 | } 143 | if (argv.minDensity < 0) { 144 | throw new Error( 145 | color.red(`Error: ${color.redBright('minDensity')} must be >= 0`), 146 | ) 147 | } 148 | if (argv.maxDensity !== undefined && isNaN(argv.maxDensity)) { 149 | throw new Error( 150 | color.red(`Error: ${color.redBright('maxDensity')} must be a number`), 151 | ) 152 | } 153 | if (argv.maxDensity < argv.minDensity) { 154 | throw new Error( 155 | color.red( 156 | `Error: ${color.redBright( 157 | 'maxDensity', 158 | )} must be greater than minDensity`, 159 | ), 160 | ) 161 | } 162 | if (isNaN(argv.delay)) { 163 | throw new Error( 164 | color.red(`Error: ${color.redBright('delay')} must be a number`), 165 | ) 166 | } 167 | if (argv.delay < 0) { 168 | throw new Error( 169 | color.red(`Error: ${color.redBright('delay')} must be >= 0`), 170 | ) 171 | } 172 | if ( 173 | argv.variationsFile && 174 | fs.existsSync(path.resolve(currentPath, argv.variationsFile)) 175 | ) { 176 | throw new Error( 177 | color.red( 178 | `Error: file ${argv.variationsFile} set with ${color.redBright( 179 | 'variationsFile', 180 | )} already exists`, 181 | ), 182 | ) 183 | } 184 | if (argv.minPercentage !== undefined && isNaN(argv.minPercentage)) { 185 | throw new Error( 186 | color.red( 187 | `Error: ${color.redBright( 188 | 'minPercentage', 189 | )} must be a number, between 0 and 1`, 190 | ), 191 | ) 192 | } 193 | if (argv.minPercentage < 0 || argv.minPercentage >= 1) { 194 | throw new Error( 195 | color.red( 196 | `Error: ${color.redBright('minPercentage')} must be >= 0 and < 1`, 197 | ), 198 | ) 199 | } 200 | if (isNaN(argv.widthsDivisor)) { 201 | throw new Error( 202 | color.red( 203 | `Error: ${color.redBright('widthsDivisor')} must be a number`, 204 | ), 205 | ) 206 | } 207 | if (isNaN(argv.widthsNumber)) { 208 | throw new Error( 209 | color.red(`Error: ${color.redBright('widthsNumber')} must be a number`), 210 | ) 211 | } 212 | if ( 213 | argv.destFile && 214 | fs.existsSync(path.resolve(currentPath, argv.destFile)) 215 | ) { 216 | throw new Error( 217 | color.red( 218 | `Error: file ${argv.destFile} set with ${color.redBright( 219 | 'destFile', 220 | )} already exists`, 221 | ), 222 | ) 223 | } 224 | if (!argv.destFile && !argv.verbose) { 225 | throw new Error( 226 | color.red( 227 | `Error: data should be either saved in a file (${color.redBright( 228 | 'destFile', 229 | )} and/or output to the console (${color.redBright('verbose')})`, 230 | ), 231 | ) 232 | } 233 | return true 234 | }) 235 | .alias('h', 'help') 236 | .help() 237 | .example( 238 | "npx $0 --statsFile ./stats.csv --url 'https://example.com/' --selector 'main img[srcset]:first-of-type' --verbose", 239 | ) 240 | .example( 241 | "npx $0 -c ./stats.csv -u 'https://example.com/' -s 'main img[srcset]:first-of-type' -i 320 -x 1280 -a ./variations.csv -f ./srcset-widths.txt -v", 242 | ) 243 | .wrap(null) 244 | .detectLocale(false).argv 245 | 246 | run(argv) 247 | -------------------------------------------------------------------------------- /src/getStats.js: -------------------------------------------------------------------------------- 1 | const csvparse = require('csv-parse/lib/sync') 2 | const fs = require('fs') 3 | const color = require('ansi-colors') 4 | const logger = require('./logger') 5 | 6 | module.exports = function getStats(csvFile, opt) { 7 | // Load content from the CSV file 8 | const statsCsv = fs.readFileSync(csvFile, 'utf8') 9 | const csvHasHeader = statsCsv.match(/[a-zA-Z]/) 10 | 11 | // Transform CSV into an array 12 | const result = csvparse(statsCsv, { 13 | columns: ['viewport', 'density', 'views'], 14 | from: csvHasHeader ? 2 : 1, 15 | cast: function (value, stats) { 16 | if (stats.column == 'density') { 17 | return parseFloat(value) 18 | } else { 19 | return parseInt(value, 10) 20 | } 21 | }, 22 | }) 23 | 24 | logger.info( 25 | `Imported ${color.green( 26 | new Intl.NumberFormat('en-US').format(result.length) + ' lines', 27 | )} of stats`, 28 | ) 29 | 30 | return result 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const util = require('util') 3 | const path = require('path') 4 | const color = require('ansi-colors') 5 | const adjustDensitiesAndViewportsWithStats = require('./adjustDensitiesAndViewportsWithStats') 6 | const getStats = require('./getStats') 7 | const browse = require('./browse') 8 | const logger = require('./logger') 9 | 10 | const writeFile = util.promisify(fs.writeFile) 11 | 12 | const NUMBER_FORMAT = new Intl.NumberFormat('en-US') 13 | 14 | const defaultOptions = { 15 | url: null, 16 | selector: 'img', 17 | statsFile: null, 18 | variationsFile: null, 19 | minDensity: null, 20 | maxDensity: null, 21 | minViewport: null, 22 | maxViewport: null, 23 | delay: 100, 24 | verbose: false, 25 | basePath: process.cwd(), 26 | minPercentage: 0.0001, 27 | widthsNumber: 5, 28 | widthsDivisor: 10, 29 | } 30 | 31 | module.exports = async function main(settings) { 32 | const options = Object.assign({}, defaultOptions, settings) 33 | 34 | logger.level = options.verbose ? 'info' : 'warn' 35 | 36 | logger.info( 37 | color.bgGreen.black( 38 | '\n Step 1: get actual stats (viewports & screen densities) of site visitors '.padEnd( 39 | 100, 40 | ) + '\n', 41 | ), 42 | ) 43 | let stats = getStats( 44 | path.resolve(options.basePath, options.statsFile), 45 | options, 46 | ) 47 | Object.assign(options, adjustDensitiesAndViewportsWithStats(stats, options)) 48 | 49 | /* ======================================================================== */ 50 | logger.info( 51 | color.bgGreen.black( 52 | '\n Step 2: get variations of image width across viewport widths '.padEnd( 53 | 100, 54 | ) + '\n', 55 | ), 56 | ) 57 | 58 | const imageWidths = await browse(options) 59 | 60 | /* ======================================================================== */ 61 | logger.info( 62 | color.bgGreen.black( 63 | '\n Step 3: compute optimal n widths from both datasets '.padEnd(100) + 64 | '\n', 65 | ), 66 | ) 67 | 68 | logger.info( 69 | color.bgBlack.greenBright.underline( 70 | '\n Step 3.1: Compute all perfect image widths '.padEnd(100) + '\n', 71 | ), 72 | ) 73 | 74 | let perfectWidths = new Map() 75 | let totalViews = 0 76 | stats.map((value) => { 77 | if ( 78 | value.density >= options.minDensity && 79 | value.density <= options.maxDensity && 80 | value.viewport >= options.minViewport && 81 | value.viewport <= options.maxViewport 82 | ) { 83 | let perfectWidth = Math.ceil( 84 | imageWidths.get(value.viewport) * value.density, 85 | ) 86 | let roundedPerfectWidth = perfectWidth 87 | if (perfectWidth % options.widthsDivisor !== 0) { 88 | roundedPerfectWidth = 89 | perfectWidth + 90 | (options.widthsDivisor - (perfectWidth % options.widthsDivisor)) 91 | } 92 | perfectWidths.set( 93 | roundedPerfectWidth, 94 | (perfectWidths.get(roundedPerfectWidth) || 0) + value.views, 95 | ) 96 | totalViews += value.views 97 | } 98 | }) 99 | let numberOfPerfectWidths = perfectWidths.size 100 | logger.info( 101 | `${color.green( 102 | NUMBER_FORMAT.format(numberOfPerfectWidths) + ' perfect widths', 103 | )} have been computed`, 104 | ) 105 | 106 | if (options.minPercentage > 0) { 107 | let numberOfPerfectWidthsWithTooFewViews = 0 108 | perfectWidths.forEach((value, key, map) => { 109 | if (value / totalViews < options.minPercentage) { 110 | perfectWidths.delete(key) 111 | numberOfPerfectWidthsWithTooFewViews++ 112 | } 113 | }) 114 | if (numberOfPerfectWidthsWithTooFewViews > 0) { 115 | logger.info( 116 | `${color.green( 117 | numberOfPerfectWidthsWithTooFewViews + 118 | ' perfect width' + 119 | (numberOfPerfectWidthsWithTooFewViews > 1 ? 's' : ''), 120 | )} with less than ${color.green( 121 | options.minPercentage * 100 + ' % views', 122 | )} ${ 123 | numberOfPerfectWidthsWithTooFewViews > 1 ? 'have' : 'has' 124 | } been removed`, 125 | ) 126 | } 127 | } 128 | 129 | // sort by decreasing views 130 | let perfectWidthsByDecreasingViews = new Map( 131 | [...perfectWidths.entries()].sort((a, b) => { 132 | if (a[1] === b[1]) { 133 | // same number of views, sort by image width 134 | return b[0] - a[0] 135 | } 136 | return b[1] - a[1] 137 | }), 138 | ) 139 | 140 | const perfectWidthsTableByViews = logger.newTable({ 141 | head: ['views', 'percentage', 'image width'], 142 | colAligns: ['right', 'right', 'right'], 143 | style: { 144 | head: ['green', 'green', 'green'], 145 | compact: true, 146 | }, 147 | }) 148 | if (perfectWidthsTableByViews) { 149 | let perfectWidthsTableByViewsLine = 1 150 | perfectWidthsByDecreasingViews.forEach((views, imageWidth) => { 151 | if (perfectWidthsTableByViewsLine <= 20) { 152 | let percentage = ((views / totalViews) * 100).toFixed(2) + ' %' 153 | perfectWidthsTableByViews.push([views, percentage, imageWidth + 'px']) 154 | perfectWidthsTableByViewsLine++ 155 | } 156 | }) 157 | if (perfectWidthsByDecreasingViews.size > 20) { 158 | perfectWidthsTableByViews.push(['…', '…', '…']) 159 | } 160 | logger.info('\nPerfect widths per decreasing views:') 161 | logger.info(perfectWidthsTableByViews.toString()) 162 | } 163 | 164 | // sort by decreasing width 165 | let perfectWidthsByDecreasingWidths = new Map( 166 | [...perfectWidths.entries()].sort((a, b) => b[0] - a[0]), 167 | ) 168 | 169 | // Show perfect widths per decreasing widths in the console only when there are 20 or less 170 | const perfectWidthsTableByWidths = logger.newTable({ 171 | head: ['image width', 'views', 'percentage'], 172 | colAligns: ['right', 'right', 'right'], 173 | style: { 174 | head: ['green', 'green', 'green'], 175 | compact: true, 176 | }, 177 | }) 178 | if (perfectWidthsTableByWidths) { 179 | let perfectWidthsTableByWidthsLine = 1 180 | perfectWidthsByDecreasingWidths.forEach((views, imageWidth) => { 181 | if (perfectWidthsTableByWidthsLine <= 20) { 182 | let percentage = ((views / totalViews) * 100).toFixed(2) + ' %' 183 | perfectWidthsTableByWidths.push([imageWidth + 'px', views, percentage]) 184 | perfectWidthsTableByWidthsLine++ 185 | } 186 | }) 187 | if (perfectWidthsByDecreasingWidths.size > 20) { 188 | perfectWidthsTableByViews.push(['…', '…', '…']) 189 | } 190 | logger.info('\nPerfect widths per decreasing widths:') 191 | logger.info(perfectWidthsTableByWidths.toString()) 192 | } 193 | 194 | logger.info( 195 | color.bgBlack.greenBright.underline( 196 | `\n Step 3.2: Find at most ${options.widthsNumber} best image widths for srcset`.padEnd( 197 | 100, 198 | ) + '\n', 199 | ), 200 | ) 201 | 202 | let optimalWidths = [] 203 | if (numberOfPerfectWidths <= options.widthsNumber) { 204 | // TODO: enhance this case 205 | logger.info( 206 | `There are already less than ${ 207 | options.widthsNumber 208 | } best widths, ${color.green('no computation necessary')}`, 209 | ) 210 | optimalWidths = [...perfectWidthsByDecreasingViews.keys()] 211 | } else { 212 | // Get all possible subset combinations in an array, with minimum and maximum lengths 213 | // Adapted from https://www.w3resource.com/javascript-exercises/javascript-function-exercise-21.php 214 | const subset = (items, min, max) => { 215 | let result_set = [] 216 | let result 217 | let loops = 0 218 | 219 | const optionsNumber = Math.pow(2, items.length) 220 | const loopsNumber = optionsNumber * items.length 221 | const loopsForOnePercent = Math.floor(loopsNumber / 100) 222 | 223 | logger.info( 224 | `There are ${color.green( 225 | NUMBER_FORMAT.format(optionsNumber) + ' potential combinations', 226 | )}`, 227 | ) 228 | 229 | const spinner = logger.newSpinner() 230 | if (spinner) { 231 | spinner.start('Starting…') 232 | } 233 | 234 | for (var x = 0; x < optionsNumber; x++) { 235 | result = [] 236 | i = items.length - 1 237 | do { 238 | if (loops % loopsForOnePercent === 0) { 239 | // Show the progress in the console 240 | if (spinner) { 241 | spinner.tick( 242 | `${color.green( 243 | (Math.floor(loops / loopsForOnePercent) + ' %').padStart(5), 244 | )} ${'' 245 | .padEnd( 246 | Math.floor(((loops / loopsForOnePercent) * 95) / 100), 247 | '#', 248 | ) 249 | .padEnd(95, '-')}`, 250 | ) 251 | } 252 | } 253 | loops++ 254 | 255 | if ((x & (1 << i)) !== 0) { 256 | result.push(items[i]) 257 | } 258 | } while (i--) 259 | 260 | if (result.length >= min && result.length <= max) { 261 | result_set.push(result) 262 | } 263 | } 264 | 265 | if (spinner) { 266 | spinner.stop( 267 | `Found ${color.green( 268 | NUMBER_FORMAT.format(result_set.length) + 269 | ' compatible combinations', 270 | )}`, 271 | ) 272 | } 273 | 274 | return result_set 275 | } 276 | 277 | // Keep only width values 278 | let widthValues = [...perfectWidthsByDecreasingWidths].map( 279 | (item) => item[0], 280 | ) 281 | // Extract the maximum width, to keep it anyway 282 | let maxWidth = widthValues.shift() 283 | // Compute subset combinations of the other sizes, and add back the max width 284 | let subsets = subset(widthValues, 0, options.widthsNumber - 1).map((data) => 285 | [maxWidth].concat(data.sort((a, b) => b - a)), 286 | ) 287 | 288 | const globalDistance = (actualWidths, subset) => { 289 | let distance = 0 290 | // Adds a floor value to the subset 291 | let subsetCopy = [...subset, 0] 292 | // Loop through the subset 293 | for (let i = 0; i < subsetCopy.length - 1; i++) { 294 | // Loop each pixel width from current to next subset value 295 | for (let j = subsetCopy[i]; j > subsetCopy[i + 1]; j--) { 296 | // If there's such an actual width, add the distance 297 | if (actualWidths.get(j)) { 298 | let additionalDistance = (subsetCopy[i] - j) * actualWidths.get(j) 299 | distance += additionalDistance 300 | } 301 | } 302 | } 303 | return distance 304 | } 305 | 306 | // Compute the distance for each subset and keep the best one 307 | const spinner = logger.newSpinner() 308 | if (spinner) { 309 | spinner.start('Starting…') 310 | } 311 | 312 | const numberOfSubsets = subsets.length 313 | let bestSubsetDistance = -1 314 | let counter = 0 315 | subsets.map((subset) => { 316 | counter++ 317 | if (spinner) { 318 | spinner.tick( 319 | `Computing distance for combination ${color.green( 320 | NUMBER_FORMAT.format(counter), 321 | )} on ${color.green(NUMBER_FORMAT.format(numberOfSubsets))}`, 322 | ) 323 | } 324 | 325 | let distance = globalDistance(perfectWidthsByDecreasingWidths, subset) 326 | if (bestSubsetDistance === -1 || distance < bestSubsetDistance) { 327 | bestSubsetDistance = distance 328 | optimalWidths = subset 329 | } 330 | }) 331 | if (spinner) { 332 | spinner.stop( 333 | `Computed distance for ${color.green( 334 | NUMBER_FORMAT.format(counter) + ' combinations', 335 | )}`, 336 | ) 337 | } 338 | } 339 | 340 | /* -------------------------- */ 341 | 342 | let srcset = [] 343 | optimalWidths.sort().forEach((width) => { 344 | srcset.push(`your/image/path.ext ${width}w`) 345 | }) 346 | 347 | if (options.verbose) { 348 | console.log( 349 | `\nHere are the best image widths for the 'srcset' attribute:\n\n${srcset.join( 350 | ',\n', 351 | )}`, 352 | ) 353 | } 354 | 355 | // Save data into the TXT file 356 | if (options.destFile) { 357 | let fileString = ` 358 | page : ${options.url} 359 | image selector : ${options.selector} 360 | widths in srcset : 361 | srcset=" 362 | ${srcset.join(',\n')}"` 363 | 364 | await writeFile( 365 | path.resolve(options.basePath, options.destFile), 366 | fileString, 367 | ) 368 | .then(() => { 369 | logger.info(color.green(`Data saved to file ${options.destFile}`)) 370 | }) 371 | .catch((error) => 372 | logger.error( 373 | `Couldn’t save data to file ${options.destFile}:\n${error}`, 374 | ), 375 | ) 376 | } 377 | 378 | return optimalWidths 379 | } 380 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const Table = require('cli-table') 2 | const color = require('ansi-colors') 3 | const Spinner = require('./spinner') 4 | 5 | class Logger { 6 | constructor() { 7 | this.level = 'warn' 8 | } 9 | shouldLog(level) { 10 | return this.levels.indexOf(level) <= this.levels.indexOf(this.level) 11 | } 12 | run(level, msg) { 13 | if (!this.shouldLog(level)) { 14 | return 15 | } 16 | if (typeof msg !== 'string') { 17 | msg = JSON.stringify(msg) 18 | } 19 | console[level](msg) 20 | } 21 | log(msg) { 22 | this.run('log', msg) 23 | } 24 | info(msg) { 25 | this.run('info', msg) 26 | } 27 | warn(msg) { 28 | this.run('warn', msg) 29 | } 30 | error(msg) { 31 | this.run('error', color.red(msg)) 32 | } 33 | newSpinner() { 34 | if (!this.shouldLog('info')) { 35 | return 36 | } 37 | return new Spinner() 38 | } 39 | newTable(cfg) { 40 | if (!this.shouldLog('info')) { 41 | return 42 | } 43 | return new Table(cfg) 44 | } 45 | } 46 | 47 | Logger.prototype.levels = ['error', 'warn', 'info', 'log'] 48 | 49 | const logger = new Logger() 50 | 51 | module.exports = logger 52 | -------------------------------------------------------------------------------- /src/spinner.js: -------------------------------------------------------------------------------- 1 | class Spinner { 2 | constructor() { 3 | this.hit = 0 4 | } 5 | tick(msg) { 6 | this.hit++ 7 | this.clearLine() 8 | process.stdout.write(msg) 9 | } 10 | clearLine() { 11 | process.stdout.clearLine() 12 | process.stdout.cursorTo(0) 13 | } 14 | start(msg) { 15 | this.hit = 0 16 | process.stdout.write(msg) 17 | } 18 | stop(msg) { 19 | process.stdout.clearLine() 20 | process.stdout.cursorTo(0) 21 | if (!msg) { 22 | return 23 | } 24 | process.stdout.write(msg + '\n') 25 | } 26 | } 27 | 28 | module.exports = Spinner 29 | --------------------------------------------------------------------------------