Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                #app
  .row
    .col.s12
      h1 Aspect Ratio Calc
  .row
    // 入力
    .section.section-input.col.s12
      form
        div
          .input-ratio
            .input-field.inline
              input#input-aspect-width.validate(type="number", min="0", required, v-model.number="ratio.width")
              label(for="input-aspect-width") width

            .input-field.inline
              input#input-aspect-height.validate(type="number", min="0", required, v-model.number="ratio.height")
              label(for="input-aspect-height") height

            .input-support
              ul
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(4, 3)") 4:3
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(3, 2)") 3:2
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(2, 1)") 2:1
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(16, 9)") 16:9
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(16, 10)") 16:10(8:5)
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(5, 4)") 5:4
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(5, 3)") 5:3
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="changeRatio(21, 9)") 21:9

        div
          .input-filter
            //- 幅のフィルター
            .input-field.inline
              input#input-width-min.validate(type="number", step="1", required, v-model.number="option.start")
              label(for="input-width-min") start(width)

            .input-support
              ul
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="addStartWidth(10)") +10
                li: a.blue-grey-text.text-lighten-2(href="#", @click.prevent="addStartWidth(-10)") -10

          //- .input-filter
          //-   .input-field.inline
          //-     select#input-number(v-model="option.number")
          //-       option(v-for="number in numbers", :value="number.value") {{ number.text }}
          //-     label(for="input-number") results
              
        //- div.clear-input
        //-   a(href="#", @click.prevent="clearSettings()").grey-text
        //-     svg(aria-hidden="true", data-prefix="fas", data-icon="trash-alt", role="img", xmlns="http://www.w3.org/2000/svg", viewBox="0 0 448 512")
        //-       path(fill="currentColor", d="M0 84V56c0-13.3 10.7-24 24-24h112l9.4-18.7c4-8.2 12.3-13.3 21.4-13.3h114.3c9.1 0 17.4 5.1 21.5 13.3L312 32h112c13.3 0 24 10.7 24 24v28c0 6.6-5.4 12-12 12H12C5.4 96 0 90.6 0 84zm416 56v324c0 26.5-21.5 48-48 48H80c-26.5 0-48-21.5-48-48V140c0-6.6 5.4-12 12-12h360c6.6 0 12 5.4 12 12zm-272 68c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208zm96 0c0-8.8-7.2-16-16-16s-16 7.2-16 16v224c0 8.8 7.2 16 16 16s16-7.2 16-16V208z")

    // 結果
    .section.section-output.col.s12
      ul.collection.with-header
        li.collection-header
          .results-header
            div
              h4
                | {{ratio.width}}
                span.result-aspect-ratio__div :
                | {{ratio.height}}
          
        li.collection-item(v-for="item in results")
          div
            .secondary-content
              .results-index
                .grey-text.text-lighten-1
                  | #
                  | {{ item.index }}
            
            .results__item-inner
              .results__ratio
                span.results__ratio-item.red-text.text-darken-1(title="width") {{ item.width | comma }}
                span.results__ratio-div.red-text.text-lighten-3
                  svg(aria-hidden="true", data-prefix="fas", data-icon="times", class="svg-inline--fa fa-times fa-w-11", role="img" xmlns="http://www.w3.org/2000/svg", viewBox="0 0 352 512")
                    path(fill="currentColor", d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z")
                span.results__ratio-item.red-text.text-darken-1(title="height") {{ item.height | comma }}
              .results__resolution
                span.grey-text(title="resolution") {{ item.resolution | comma }} ({{ item.resolution | about }})
                
      .row
        .col.s12.center-align
          a(@click="awardPageTop").btn-flat
            i.material-icons arrow_upward
              
            
!

CSS

              
                @use postcss-simple-vars;
@use postcss-nested;

body {
  margin: 20px;
  background-color: #f5f5f5;
}


h1 {
  font-size: 2rem;
}


.section {
  
  /* 見出し */
  .header {
    font-size: 1.3em;
  }
  
  /* clearfix */
  &:after {
    content: '';
    display: block;
    clear: both;
  }
  
/*   &:not(:nth-last-of-type(1)) {
    margin-bottom: 40px;
  } */
}

form {
  position: relative;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  display: flex;
  
  @media screen and (max-width: 479px) {
    flex-direction: column;
    
    > div {
      &:not(:last-of-type) {
        margin-bottom: 1rem;
      }
    }
  }
  
  @media screen and (min-width: 480px) {
    > div {
      &:not(:last-of-type) {
        margin-right: 30px;
      }
    }
  }
}

  
/* 入力フィールド */
.section-input {
  position: sticky;
  top: 0;
  background-color: #f5f5f5;
  z-index: 1;
}

.input-ratio {
  
  .input-field {
    width: 80px;
    
    &:not(:last-of-type) {
      margin-right: 0.5rem;
    }
    
    &:last-of-type {
      width: 200px;
    }
  }
}


/* 入力補助 */
.input-support {
  ul {
    margin-top: -0.5rem;
    margin-bottom: 0;
    
    li {
      display: inline-block;
      
      &:not(:last-of-type) {
        margin-right: 0.5rem;
      }
    }
  }
}


.input-filter {
  display: inline-block;
  vertical-align: top;
  
  .input-field {
    width: 80px;
    
    &:not(:last-of-type) {
      margin-right: 0.5rem;
    }
  }
}


.clear-input {
  margin-left: auto;
  
  svg {
    width: 1rem;
  }
}


/* 計算結果のアスペクト比 */
.result-aspect-ratio__div {
  padding-left: 0.2em;
  padding-right: 0.2em;
}


.results-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
}

.result-num-selector {
  display: inline-block;
}


/* 出力結果 */
#results {
  width: auto;
}

.results__item-inner {
  display: flex;
  align-items: center;
  
  @media screen and (max-width: 639px) {
    flex-direction: column;
    align-items: flex-start;
  }
}

.collection-item {
  &:nth-child(even) {
    background-color: #fafafa;
  }
}

.results__ratio {
  font-size: 1.25rem;
  
  @media screen and (max-width: 639px) {
    margin-bottom: 0.2rem;
  }
  
  @media screen and (min-width: 640px) {
    margin-right: 1.5rem;
  }
  
  
  span {
    &:not(:last-of-type) {
      margin-right: 0.4rem;
    }
  }
}


.results__ratio-div {
  pointer-events: none;
  
  svg {
    width: 0.5rem;
  }
}
              
            
!

JS

              
                const defaultValue = {
  ratio: {
    width: 4,
    height: 3
  },
  option: {
    start: 100
  },
  rows: 100,
  // 無限スクロール用のインスタンス
  intersectionObserver: undefined,
  results: []
};
let data;

try {
  // 設定がcookieにあれば復元する
  // Firefoxで 'The operation is insecure. js.cookie' になるのでtryで回避する
  const loadSetteings = Cookies.getJSON('settings');

  // Object.assignだとobjectの値がないのも上書きされてしまうので、_.merge()を使う
  data = _.merge(defaultValue, loadSetteings);
} catch (e) {
  data = defaultValue;
}


const app = new Vue({
  el: '#app',
  data,

  mounted() {
    console.log('mounted');
    const observer = new IntersectionObserver(this.handleIntersect, {
      root: null, // page root
      rootMargin: '0px',
      threshold: 1
    });
    this.intersectionObserver = observer;

    this.setupIntersect();
  },

  computed: {
    /**
     * 計算結果
     *
     */
    results() {
      let sizes = calcSizeByRatio(parseFloat(this.ratio.width), parseFloat(this.ratio.height), this.option.start, this.rows);  // 計算結果

      // 結果にindex、画素数を付加する
      sizes.forEach(function (val, index) {
        val.index = index + 1;
        val.resolution = val.width * val.height;
      });

      return sizes;
    }
  },

  methods: {
    /**
     * アスペクト比を変更する
     *
     * @param ratioWidth 幅の比率
     * @param ratioHeight 縦の比率
     */
    changeRatio(ratioWidth, ratioHeight) {
      this.ratio.width = ratioWidth;
      this.ratio.height = ratioHeight;
    },


    /**
     * 開始幅を増減する
     *
     * @param value {number} 増減値
     */
    addStartWidth(value) {
      this.option.start += value;
    },

    /**
     * 設定をクッキーに保存する
     *
     */
    saveSettings() {
      try {
        Cookies.set('settings', {
          ratio: this.ratio,
          option: {
            start: this.option.start
          }
        }, {
          expires: 7
        });
      } catch (e) {
        console.error(e);
      }
    },

    /**
     * 件数を増やす
     */
    incrementRows() {
      this.rows = this.rows + 100;
    },

    /**
     * 件数をリセット
     */
    resetRows() {
      this.rows = 100;
    },

    /**
     * 無限スクロールの設定
     */
    setupIntersect() {
      const targets = document.querySelectorAll('.collection-item');
      const len = targets.length - 1;

      // 1000件まで無限スクロール
      // 件数増えるとレンダリング?で結構重くなるので
      if (this.rows < 1000) {
        this.intersectionObserver.observe(targets[len]);
      }
    },

    /**
     * 閾値をまたいだ
     * 
     * @param entries
     * @param observer
     */
    handleIntersect(entries, observer) {
      console.log('handleIntersect', entries, observer);
      entries.forEach((entry) => {
        console.log('entry', entry);
        if (Math.round(entry.intersectionRatio * 100) / 100 >= 0.75) {
          // 件数を増やす
          this.incrementRows();

          observer.unobserve(entry.target);
        }
      });
    },

    /**
     * 設定をデフォルトに設定する
     *
     */
    clearSettings () {
      Cookies.remove('settings');

      this.ratio.width = defaultValue.ratio.width;
      this.ratio.height = defaultValue.ratio.height;
      this.option.start = defaultValue.option.start;
    },

    /**
     * ページトップに戻る
     */
    awardPageTop () {
      window.scrollTo({
        top: 0,
        behavior: "smooth"
      });
    }
  },

  filters: {

    /**
     * 数値を丸める
     *
     * @param num 数値
     * @return {string} 3桁区切りの文字列
     */
    about(num) {
      let result = `${num}`;
      // 1K 1,000
      // 1M 1,000,000
      // 1B 1,000,000,000
      // 1T 1,000,000,000,000
      if (num > 1000000000000) {
        let number = Math.round(num / 1000000000000 * 10) / 10;
        result = `${String(number).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,')} T`;  // 数値はカンマ区切り
      }
      else if (num > 1000000000) {
        result = `${Math.round(num / 1000000000 * 10) / 10} B`;
      }
      else if (num > 1000000) {
        result = `${Math.round(num / 1000000 * 10) / 10} M`;
      }
      else if (num > 1000) {
        result = `${Math.round(num / 1000 * 10) / 10} K`;
      }

      return result;
    },

    /**
     * 3桁ごとにカンマ区切り
     *
     * @param num 数値
     * @return {string} 3桁区切りの文字列
     */
    comma(num) {
      return String(num).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');
    }
  },

  watch: {
    'ratio.width': function () {
      this.resetRows();
      this.saveSettings();
    },

    'ratio.height': function () {
      this.resetRows();
      this.saveSettings();
    },

    'option.start': function () {
      this.resetRows();
      this.saveSettings();
    },

    'results': function () {
      Vue.nextTick(() => {
        this.setupIntersect();
      });
    }
  }
});



/**
 * 比率で割り切れるサイズを計算する
 * 
 * @param ratioWidth {number} 幅の比率
 * @param ratioHeight {number} 高さの比率
 * @param width {number} 幅のピクセル数
 * @param number {number} 件数
 * @return 検索結果
 */
function calcSizeByRatio(ratioWidth = 4, ratioHeight = 3, width = 100, number) {
  let result = [];  // 結果
  let count = 0;  // 見つかった件数
  const limitWidth = width + 1000000; // 検索を終了させる幅。結果の件数が指定より少ない場合に無限ループになるのを回避するため
  const ratio = ratioHeight / ratioWidth;

  if (ratioWidth > 0 && ratioHeight > 0) {
    // 件数に満たない場合も、明らかに見つからない場合はループを終了させる
    for (let i = width; count < number && i <= limitWidth; i++) {
      let h = i * ratio;
      if (h === Math.floor(h) && h !== 0) {
        //高さが割り切れるなら結果に追加
        result[result.length] = {
          width: i,
          height: h
        };
        count++;
      }
    }
  }

  return result;
}

              
            
!
999px

Console