<commit_before>import React, { Suspense, lazy } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Input from '@material-ui/core/Input'
import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import SearchIcon from '@material-ui/icons/Search'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Paper from '@material-ui/core/Paper'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { isSearchPageEnabled } from 'js/utils/feature-flags'
import {
  adblockerWhitelistingForSearchURL,
  dashboardURL,
  modifyURLParams,
} from 'js/navigation/navigation'
import { externalRedirect } from 'js/navigation/utils'
import LogoWithText from 'js/components/Logo/LogoWithText'
import { parseUrlSearchString } from 'js/utils/utils'
import SearchResults from 'js/components/Search/SearchResults'
import { isReactSnapClient } from 'js/utils/search-utils'
import SearchMenuQuery from 'js/components/Search/SearchMenuQuery'
import detectAdblocker from 'js/utils/detectAdblocker'
import Link from 'js/components/General/Link'

const Footer = lazy(() => import('js/components/General/Footer'))

const searchBoxBorderColor = '#ced4da'
const searchBoxBorderColorFocused = '#bdbdbd'

const styles = theme => ({
  inputRootStyle: {
    padding: 0,
    borderRadius: 28,
    backgroundColor: theme.palette.common.white,
    border: `1px solid ${searchBoxBorderColor}`,
    fontSize: 16,
    boxShadow: '0rem 0rem 0.02rem 0.02rem rgba(0, 0, 0, 0.1)',
    transition: theme.transitions.create(['border-color', 'box-shadow']),
    '&:hover': {
      borderColor: searchBoxBorderColorFocused,
      boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
    },
  },
  inputRootFocused: {
    borderColor: searchBoxBorderColorFocused,
    boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
  },
  inputStyle: {
    padding: '12px 16px',
  },
  tabsContainerRootStyle: {
    minHeight: 0,
  },
  tabRootStyle: {
    color: '#505050', // Same as search result description
    minHeight: 34,
    minWidth: 10,
  },
})

class SearchPage extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isAdBlockerEnabled: false,
      query: '',
      searchFeatureEnabled: isSearchPageEnabled(),
      searchSource: null,
      searchText: '',
      showPlaceholderText: false,
    }
  }

  componentDidMount() {
    if (!this.state.searchFeatureEnabled) {
      // Cannot use pushState now that the apps are separate.
      externalRedirect(dashboardURL)
    }
    const { location } = this.props

    // Wait until after mount to update prerendered state.
    const query = parseUrlSearchString(location.search).q || ''
    this.setState({
      // We always derive the query and page values URL parameter
      // values. We keep tehse in state so that we update the
      // prerendered components after mount, because at prerender
      // time we do not know the query string. We can remove this
      // from state if we switch to server-side rendering.
      query: query,
      page: this.getPageNumberFromSearchString(location.search),
      showPlaceholderText: !isReactSnapClient(),
      searchSource: parseUrlSearchString(location.search).src || null,
      searchText: query,
    })

    // AdBlockerDetection
    detectAdblocker()
      .then(isEnabled => {
        this.setState({
          isAdBlockerEnabled: isEnabled,
        })
      })
      .catch(e => {
        console.error(e)
      })
  }

  componentDidUpdate(prevProps) {
    const { location } = this.props
    const currentQuery = parseUrlSearchString(location.search).q
    const prevQuery = parseUrlSearchString(prevProps.location.search).q
    if (currentQuery !== prevQuery) {
      this.setState({
        query: currentQuery,
        searchText: currentQuery,
      })
    }

    // Check if the page number has changed.
    const currentPage = this.getPageNumberFromSearchString(location.search)
    const prevPage = this.getPageNumberFromSearchString(
      prevProps.location.search
    )
    if (currentPage !== prevPage) {
      this.setState({
        page: currentPage,
      })
    }
  }

  /**
   * Take a search string, such as ?abc=hi&p=12, and return the
   * integer value of the "page" URL parameter. If the parameter is
   * not set or is not an integer, return 1.
   * @param {String} searchStr - The URL parameter string,
   *   such as '?myParam=foo&another=bar'
   * @return {Number} The search results page inded
   */
  getPageNumberFromSearchString(searchStr) {
    return parseInt(parseUrlSearchString(searchStr).page, 10) || 1
  }

  search() {
    const newQuery = this.state.searchText
    if (newQuery) {
      modifyURLParams({
        page: 1,
        q: newQuery,
        src: 'self',
      })
      this.setState({
        searchSource: 'self',
      })
    }
  }

  onSearchTextChange(e) {
    this.setState({
      searchText: e.target.value,
    })
  }

  render() {
    const { classes } = this.props

    // For demonstration only
    let isAdBlockerEnabled = true
    const {
      // isAdBlockerEnabled,
      page,
      query,
      searchSource,
      searchText,
    } = this.state
    const queryEncoded = query ? encodeURI(query) : ''
    const searchResultsPaddingLeft = 170
    if (!this.state.searchFeatureEnabled) {
      return null
    }
    return (
      <div
        data-test-id={'search-page'}
        style={{
          backgroundColor: '#fff',
          // For demonstration only
          minWidth: 1100,
          // minWidth: '100vw',
          minHeight: '100vh',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div
          style={{
            backgroundColor: '#fff',
            borderBottom: '1px solid #e4e4e4',
          }}
        >
          <div
            style={{
              padding: '20px 18px 6px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-start',
            }}
          >
            <LogoWithText
              style={{
                width: 100,
                height: 36,
              }}
            />
            <div
              style={{
                maxWidth: 600,
                marginLeft: 30,
                flex: 1,
              }}
            >
              <Input
                id="search-input"
                type={'text'}
                value={searchText}
                onChange={this.onSearchTextChange.bind(this)}
                onKeyPress={e => {
                  if (e.key === 'Enter') {
                    this.search()
                  }
                }}
                placeholder={
                  // Don't immediately render the placeholder text because
                  // we may rapidly replace it with the query on first render.
                  this.state.showPlaceholderText
                    ? 'Search to raise money for charity...'
                    : ''
                }
                disableUnderline
                fullWidth
                classes={{
                  root: classes.inputRootStyle,
                  input: classes.inputStyle,
                  focused: classes.inputRootFocused,
                }}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="Search button"
                      onClick={this.search.bind(this)}
                    >
                      <SearchIcon
                        style={{ color: searchBoxBorderColorFocused }}
                      />
                    </IconButton>
                  </InputAdornment>
                }
              />
            </div>
            <SearchMenuQuery
              style={{
                marginLeft: 'auto',
              }}
            />
          </div>
          <Tabs
            value={0}
            indicatorColor={'secondary'}
            style={{
              marginTop: 8,
              marginLeft: 149,
            }}
            classes={{
              root: classes.tabsContainerRootStyle,
            }}
          >
            <Tab
              label="Web"
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Images"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=isch`
                  : 'https://images.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="News"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=nws`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Videos"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=vid`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Maps"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/maps/?q=${queryEncoded}`
                  : 'https://www.google.com/maps'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
          </Tabs>
        </div>
        <div
          style={{
            display: 'flex',
          }}
        >
          <div
            style={{
              marginLeft: searchResultsPaddingLeft,
              marginTop: 20,
              width: 600,
            }}
          >
            {isAdBlockerEnabled ? (
              <div
                data-test-id={'search-prevented-warning'}
                style={{
                  marginBottom: 20,
                  width: '100%',
                }}
              >
                <Paper
                  style={{
                    padding: '10px 18px',
                    backgroundColor: 'rgb(242, 222, 222)',
                  }}
                >
                  <span
                    style={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'flex-start',
                    }}
                  >
                    <Typography
                      style={{
                        color: 'rgb(169, 68, 66)',
                        fontWeight: 'bold',
                        marginBottom: 8,
                        marginTop: 8,
                      }}
                      variant={'h6'}
                    >
                      Please disable your ad blocker
                    </Typography>
                    <Typography variant={'body2'}>
                      We use search ads to raise money for charity. You'll
                      likely need to whitelist Search for a Cause for search
                      results to show.
                    </Typography>
                    <div
                      style={{
                        display: 'flex',
                        alignSelf: 'flex-end',
                        marginTop: 10,
                      }}
                    >
                      <Link
                        to={adblockerWhitelistingForSearchURL}
                        target="_blank"
                        rel="noopener noreferrer"
                      >
                        <Button color={'default'}>Show me how</Button>
                      </Link>
                    </div>
                  </span>
                </Paper>
              </div>
            ) : null}
            <SearchResults
              query={query}
              page={page}
              onPageChange={newPageIndex => {
                modifyURLParams({
                  page: newPageIndex,
                })
              }}
              searchSource={searchSource}
              style={{
                maxWidth: 600,
                marginBottom: 40,
                height: 700,
                // For demonstration only
                backgroundColor: 'aqua',
              }}
            />
          </div>
          <div
            data-test-id={'search-sidebar'}
            style={{
              display: 'flex',
              flexDirection: 'column',
              margin: '20px 40px',
              width: '100%',
              boxSizing: 'border-box',
              maxWidth: 410,
              minWidth: 300,
              // For demonstration only
              backgroundColor: 'green',
            }}
          >
            <div
              style={{
                // For demonstration only
                height: 500,
                width: '100%',
                backgroundColor: 'purple',
              }}
            >
              some stuff here
            </div>
            <Paper
              style={{
                width: '100%',
                boxSizing: 'border-box',
                padding: '10px 18px',
              }}
            >
              <span
                style={{
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'flex-start',
                }}
              >
                <Typography variant={'h6'}>Your searches do good!</Typography>
                <Typography variant={'body2'}>
                  When you search, you raise money for charity! The money comes
                  from the ads in search results, and you decide where the money
                  goes by donating your hearts to your favorite nonprofit.
                </Typography>
                <div
                  style={{
                    display: 'flex',
                    alignSelf: 'flex-end',
                    marginTop: 10,
                  }}
                >
                  <Button color={'primary'}>Got it!</Button>
                </div>
              </span>
            </Paper>
          </div>
        </div>
        <Suspense fallback={null}>
          <Footer
            style={{
              marginTop: 'auto',
            }}
          />
        </Suspense>
      </div>
    )
  }
}

SearchPage.propTypes = {
  classes: PropTypes.object.isRequired,
  location: PropTypes.shape({
    search: PropTypes.string.isRequired,
  }),
  theme: PropTypes.object.isRequired,
}

SearchPage.defaultProps = {}

export default withStyles(styles, { withTheme: true })(SearchPage)
<commit_msg>Add smiley face<commit_after>import React, { Suspense, lazy } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Input from '@material-ui/core/Input'
import IconButton from '@material-ui/core/IconButton'
import InputAdornment from '@material-ui/core/InputAdornment'
import SearchIcon from '@material-ui/icons/Search'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Paper from '@material-ui/core/Paper'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { isSearchPageEnabled } from 'js/utils/feature-flags'
import {
  adblockerWhitelistingForSearchURL,
  dashboardURL,
  modifyURLParams,
} from 'js/navigation/navigation'
import { externalRedirect } from 'js/navigation/utils'
import LogoWithText from 'js/components/Logo/LogoWithText'
import { parseUrlSearchString } from 'js/utils/utils'
import SearchResults from 'js/components/Search/SearchResults'
import { isReactSnapClient } from 'js/utils/search-utils'
import SearchMenuQuery from 'js/components/Search/SearchMenuQuery'
import detectAdblocker from 'js/utils/detectAdblocker'
import Link from 'js/components/General/Link'

const Footer = lazy(() => import('js/components/General/Footer'))

const searchBoxBorderColor = '#ced4da'
const searchBoxBorderColorFocused = '#bdbdbd'

const styles = theme => ({
  inputRootStyle: {
    padding: 0,
    borderRadius: 28,
    backgroundColor: theme.palette.common.white,
    border: `1px solid ${searchBoxBorderColor}`,
    fontSize: 16,
    boxShadow: '0rem 0rem 0.02rem 0.02rem rgba(0, 0, 0, 0.1)',
    transition: theme.transitions.create(['border-color', 'box-shadow']),
    '&:hover': {
      borderColor: searchBoxBorderColorFocused,
      boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
    },
  },
  inputRootFocused: {
    borderColor: searchBoxBorderColorFocused,
    boxShadow: '0rem 0.05rem 0.2rem 0.05rem rgba(0, 0, 0, 0.1)',
  },
  inputStyle: {
    padding: '12px 16px',
  },
  tabsContainerRootStyle: {
    minHeight: 0,
  },
  tabRootStyle: {
    color: '#505050', // Same as search result description
    minHeight: 34,
    minWidth: 10,
  },
})

class SearchPage extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      isAdBlockerEnabled: false,
      query: '',
      searchFeatureEnabled: isSearchPageEnabled(),
      searchSource: null,
      searchText: '',
      showPlaceholderText: false,
    }
  }

  componentDidMount() {
    if (!this.state.searchFeatureEnabled) {
      // Cannot use pushState now that the apps are separate.
      externalRedirect(dashboardURL)
    }
    const { location } = this.props

    // Wait until after mount to update prerendered state.
    const query = parseUrlSearchString(location.search).q || ''
    this.setState({
      // We always derive the query and page values URL parameter
      // values. We keep tehse in state so that we update the
      // prerendered components after mount, because at prerender
      // time we do not know the query string. We can remove this
      // from state if we switch to server-side rendering.
      query: query,
      page: this.getPageNumberFromSearchString(location.search),
      showPlaceholderText: !isReactSnapClient(),
      searchSource: parseUrlSearchString(location.search).src || null,
      searchText: query,
    })

    // AdBlockerDetection
    detectAdblocker()
      .then(isEnabled => {
        this.setState({
          isAdBlockerEnabled: isEnabled,
        })
      })
      .catch(e => {
        console.error(e)
      })
  }

  componentDidUpdate(prevProps) {
    const { location } = this.props
    const currentQuery = parseUrlSearchString(location.search).q
    const prevQuery = parseUrlSearchString(prevProps.location.search).q
    if (currentQuery !== prevQuery) {
      this.setState({
        query: currentQuery,
        searchText: currentQuery,
      })
    }

    // Check if the page number has changed.
    const currentPage = this.getPageNumberFromSearchString(location.search)
    const prevPage = this.getPageNumberFromSearchString(
      prevProps.location.search
    )
    if (currentPage !== prevPage) {
      this.setState({
        page: currentPage,
      })
    }
  }

  /**
   * Take a search string, such as ?abc=hi&p=12, and return the
   * integer value of the "page" URL parameter. If the parameter is
   * not set or is not an integer, return 1.
   * @param {String} searchStr - The URL parameter string,
   *   such as '?myParam=foo&another=bar'
   * @return {Number} The search results page inded
   */
  getPageNumberFromSearchString(searchStr) {
    return parseInt(parseUrlSearchString(searchStr).page, 10) || 1
  }

  search() {
    const newQuery = this.state.searchText
    if (newQuery) {
      modifyURLParams({
        page: 1,
        q: newQuery,
        src: 'self',
      })
      this.setState({
        searchSource: 'self',
      })
    }
  }

  onSearchTextChange(e) {
    this.setState({
      searchText: e.target.value,
    })
  }

  render() {
    const { classes } = this.props

    // For demonstration only
    let isAdBlockerEnabled = true
    const {
      // isAdBlockerEnabled,
      page,
      query,
      searchSource,
      searchText,
    } = this.state
    const queryEncoded = query ? encodeURI(query) : ''
    const searchResultsPaddingLeft = 170
    if (!this.state.searchFeatureEnabled) {
      return null
    }
    return (
      <div
        data-test-id={'search-page'}
        style={{
          backgroundColor: '#fff',
          // For demonstration only
          minWidth: 1100,
          // minWidth: '100vw',
          minHeight: '100vh',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <div
          style={{
            backgroundColor: '#fff',
            borderBottom: '1px solid #e4e4e4',
          }}
        >
          <div
            style={{
              padding: '20px 18px 6px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-start',
            }}
          >
            <LogoWithText
              style={{
                width: 100,
                height: 36,
              }}
            />
            <div
              style={{
                maxWidth: 600,
                marginLeft: 30,
                flex: 1,
              }}
            >
              <Input
                id="search-input"
                type={'text'}
                value={searchText}
                onChange={this.onSearchTextChange.bind(this)}
                onKeyPress={e => {
                  if (e.key === 'Enter') {
                    this.search()
                  }
                }}
                placeholder={
                  // Don't immediately render the placeholder text because
                  // we may rapidly replace it with the query on first render.
                  this.state.showPlaceholderText
                    ? 'Search to raise money for charity...'
                    : ''
                }
                disableUnderline
                fullWidth
                classes={{
                  root: classes.inputRootStyle,
                  input: classes.inputStyle,
                  focused: classes.inputRootFocused,
                }}
                endAdornment={
                  <InputAdornment position="end">
                    <IconButton
                      aria-label="Search button"
                      onClick={this.search.bind(this)}
                    >
                      <SearchIcon
                        style={{ color: searchBoxBorderColorFocused }}
                      />
                    </IconButton>
                  </InputAdornment>
                }
              />
            </div>
            <SearchMenuQuery
              style={{
                marginLeft: 'auto',
              }}
            />
          </div>
          <Tabs
            value={0}
            indicatorColor={'secondary'}
            style={{
              marginTop: 8,
              marginLeft: 149,
            }}
            classes={{
              root: classes.tabsContainerRootStyle,
            }}
          >
            <Tab
              label="Web"
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Images"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=isch`
                  : 'https://images.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="News"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=nws`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Videos"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/search?q=${queryEncoded}&tbm=vid`
                  : 'https://www.google.com'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
            <Tab
              label="Maps"
              target="_top"
              href={
                queryEncoded
                  ? `https://www.google.com/maps/?q=${queryEncoded}`
                  : 'https://www.google.com/maps'
              }
              classes={{
                root: classes.tabRootStyle,
              }}
            />
          </Tabs>
        </div>
        <div
          style={{
            display: 'flex',
          }}
        >
          <div
            style={{
              marginLeft: searchResultsPaddingLeft,
              marginTop: 20,
              width: 600,
            }}
          >
            {isAdBlockerEnabled ? (
              <div
                data-test-id={'search-prevented-warning'}
                style={{
                  marginBottom: 20,
                  width: '100%',
                }}
              >
                <Paper
                  style={{
                    padding: '10px 18px',
                    backgroundColor: 'rgb(242, 222, 222)',
                  }}
                >
                  <span
                    style={{
                      display: 'flex',
                      flexDirection: 'column',
                      alignItems: 'flex-start',
                    }}
                  >
                    <Typography
                      style={{
                        color: 'rgb(169, 68, 66)',
                        fontWeight: 'bold',
                        marginBottom: 8,
                        marginTop: 8,
                      }}
                      variant={'h6'}
                    >
                      Please disable your ad blocker
                    </Typography>
                    <Typography variant={'body2'}>
                      We use search ads to raise money for charity. You'll
                      likely need to whitelist Search for a Cause for search
                      results to show.
                    </Typography>
                    <div
                      style={{
                        display: 'flex',
                        alignSelf: 'flex-end',
                        marginTop: 10,
                      }}
                    >
                      <Link
                        to={adblockerWhitelistingForSearchURL}
                        target="_blank"
                        rel="noopener noreferrer"
                      >
                        <Button color={'default'}>Show me how</Button>
                      </Link>
                    </div>
                  </span>
                </Paper>
              </div>
            ) : null}
            <SearchResults
              query={query}
              page={page}
              onPageChange={newPageIndex => {
                modifyURLParams({
                  page: newPageIndex,
                })
              }}
              searchSource={searchSource}
              style={{
                maxWidth: 600,
                marginBottom: 40,
                height: 700,
                // For demonstration only
                backgroundColor: 'aqua',
              }}
            />
          </div>
          <div
            data-test-id={'search-sidebar'}
            style={{
              display: 'flex',
              flexDirection: 'column',
              margin: '20px 40px',
              width: '100%',
              boxSizing: 'border-box',
              maxWidth: 410,
              minWidth: 300,
              // For demonstration only
              backgroundColor: 'green',
            }}
          >
            <div
              style={{
                // For demonstration only
                height: 500,
                width: '100%',
                backgroundColor: 'purple',
              }}
            >
              some stuff here
            </div>
            <Paper
              style={{
                width: '100%',
                boxSizing: 'border-box',
                padding: '10px 18px',
              }}
            >
              <span
                style={{
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'flex-start',
                }}
              >
                <Typography variant={'h6'}>Your searches do good :)</Typography>
                <Typography variant={'body2'}>
                  When you search, you raise money for charity! The money comes
                  from the ads in search results, and you decide where the money
                  goes by donating your hearts to your favorite nonprofit.
                </Typography>
                <div
                  style={{
                    display: 'flex',
                    alignSelf: 'flex-end',
                    marginTop: 10,
                  }}
                >
                  <Button color={'primary'}>Got it!</Button>
                </div>
              </span>
            </Paper>
          </div>
        </div>
        <Suspense fallback={null}>
          <Footer
            style={{
              marginTop: 'auto',
            }}
          />
        </Suspense>
      </div>
    )
  }
}

SearchPage.propTypes = {
  classes: PropTypes.object.isRequired,
  location: PropTypes.shape({
    search: PropTypes.string.isRequired,
  }),
  theme: PropTypes.object.isRequired,
}

SearchPage.defaultProps = {}

export default withStyles(styles, { withTheme: true })(SearchPage)
<|endoftext|>


<commit_before>const test = require('ava');
const random = require('./helpers/random');
const all = require('./products');

test('all()', t => {
  t.true(all.length < 0);
});

test('random()', t => {
  t.truthy(random(all));
  t.not(random(all), random(all));
});
<commit_msg>🤖(break): 2017-6-13 21:01:03<commit_after>const test = require('ava');
const random = require('./helpers/random');
const all = require('./products');

test('all()', t => {
  t.true(all.length > 0);
});

test('random()', t => {
  t.truthy(random(all));
  t.not(random(all), random(all));
});
<|endoftext|>


<commit_before>var processingMessage = false;
//can I currently send an updated pose to other devices?
var canSend           = false;

function initializeModelViewer() {
  showAllQuick(document.getElementById('viewer_object').runtime);
  document.getElementById('viewport').addEventListener('viewpointChanged', viewpointChanged);
  document.getElementById('viewer_object').addEventListener('mousedown', enableSendingOnFirstClick);
}

function receiveViewpointMsg(extras){
    //disable listening for changes in the viewport
    processingMessage = true;

    var cam = printPositionAndOrientation('Received', extras.position, extras.orientation);

    //apply new viewpoint
    document.getElementById('viewport').setAttribute('position', cam.pos);
    document.getElementById('viewport').setAttribute('orientation', cam.rot);

    //clear any previous timeout
    if(typeof reenableViewpointListeningTimeout != 'undefined'){
  clearTimeout(reenableViewpointListeningTimeout);
    }
    //enable listening for changes in the viewport again some milliseconds after the last received msg
    reenableViewpointListeningTimeout = setTimeout(function(){processingMessage = false;} , 1000);
}

// Viewport section.
function sendPositionAndOrientation(pos, rot) {
  //only send if there is a role environment the viewer is embedded in
  if(!isEmbeddedInRole){
    return;
  }

  // Send through IWC!
  var viewpointMsg = {'position': pos, 'orientation': rot}

  //send to wrapper
  roleWrapper.postMessage("ViewpointUpdate " + JSON.stringify(viewpointMsg), "*");
}

// Update position and rotation of the camera
function viewpointChanged(evt) {

  // Prevent widgets from sending updates again and again
  // If we set the position because we received a message we do not want to send it back
  if(!evt || processingMessage) {
    return;
  }

  // You can only send data once every 0.1 seconds.
  if(!canSend) {
    document.getElementById('debugText').innerHTML = "Bypassing send!";
    console.log("Bypassing send!");
    return;
  }
  canSend     = false;
  if(typeof sendTimeout != 'undefined'){
    clearTimeout(sendTimeout);
  }
  sendTimeout = setTimeout(function(){
    canSend = true;
    document.getElementById('debugText').innerHTML = "Can send again!";
    console.log("Can send again!");
  } , 100);

  printPositionAndOrientation('Updated', evt.position, evt.orientation);
  sendPositionAndOrientation(evt.position, evt.orientation);

  var intent = {
    'component' :'',                            // recipient, empty for broadcast
    'data'      :'http://data.org/some/data',   // data as URI
    'dataType'  :'text/xml',                    // data mime type
    'action'    :'ACTION_UPDATE',               // action to be performed by receivers
    'flags'     :['PUBLISH_GLOBAL'],            // control flags
    'extras'    :{'position': evt.position, 'orientation': evt.orientation}, // optional auxiliary data
  }

  console.info("subsite: intent: ", intent);
}

// Converts the position and orientation into a string and updates the view to display it.
function printPositionAndOrientation(str, pos, rot) {
  var camPos = pos.x.toFixed(4) + ' ' + pos.y.toFixed(4) + ' ' + pos.z.toFixed(4);
  var camRot = rot[0].x.toFixed(4) + ' ' + rot[0].y.toFixed(4) + ' '
                + rot[0].z.toFixed(4) + ' ' + rot[1].toFixed(4);
  document.getElementById('debugText').innerHTML = str + ' viewpoint position = ' +
                                                    camPos + ' orientation = ' + camRot;

  return {'pos': camPos, 'rot': camRot};
}

function showAllQuick(runtime, axis) {
  if (axis === undefined)
    axis = "negZ";

  var scene = runtime.canvas.doc._viewarea._scene;
  scene.updateVolume();

  var min = x3dom.fields.SFVec3f.copy(scene._lastMin);
  var max = x3dom.fields.SFVec3f.copy(scene._lastMax);

  var x = "x", y = "y", z = "z";
  var sign = 1;
  var to, from = new x3dom.fields.SFVec3f(0, 0, -1);

  switch (axis) {
      case "posX":
      sign = -1;
      case "negX":
      z = "x"; x = "y"; y = "z";
      to = new x3dom.fields.SFVec3f(sign, 0, 0);
      break;
      case "posY":
      sign = -1;
      case "negY":
      z = "y"; x = "z"; y = "x";
      to = new x3dom.fields.SFVec3f(0, sign, 0);
      break;
      case "posZ":
      sign = -1;
      case "negZ":
      default:
      to = new x3dom.fields.SFVec3f(0, 0, -sign);
      break;
  }

  var viewpoint = scene.getViewpoint();
  var fov = viewpoint.getFieldOfView();

  var dia = max.subtract(min);

  var diaz2 = dia[z] / 2.0, tanfov2 = Math.tan(fov / 2.0);

  var dist1 = (dia[y] / 2.0) / tanfov2 + diaz2;
  var dist2 = (dia[x] / 2.0) / tanfov2 + diaz2;

  dia = min.add(dia.multiply(0.5));

  dia[z] += sign * (dist1 > dist2 ? dist1 : dist2) * 1.01;

  var quat = x3dom.fields.Quaternion.rotateFromTo(from, to);

  var viewmat = quat.toMatrix();
  viewmat = viewmat.mult(x3dom.fields.SFMatrix4f.translation(dia.negate()));

  runtime.canvas.doc._viewarea._scene.getViewpoint().setView(viewmat);
};

/**
 * Enable sending of the updated pose after the first click.
 * This prevents sending a pose when opening the model and the view get automatically updated.
 */
function enableSendingOnFirstClick(evt){
    canSend = true;
    document.getElementById('viewer_object').removeEventListener('mousedown', enableSendingOnFirstClick);
}
<commit_msg>[Viewer]: The bug with synchronized views not matching fixed. Transform matrices are now sent over network instead of individual transformations. This fixes the synchronization bug.<commit_after>var processingMessage = false;
var canSend           = true;
var lastPos           = "";
var lastRot           = "";

function initializeModelViewer() {
  showAllQuick(document.getElementById('viewer_object').runtime);
  document.getElementById('viewport').addEventListener('viewpointChanged', viewpointChanged);
  document.getElementById('viewer_object').addEventListener('mouseup', viewerMouseUp, false);
}

function receiveViewpointMsg(extras){
    //disable listening for changes in the viewport
    processingMessage = true;

    //var cam = printPositionAndOrientation('Received', extras.position, extras.orientation);

    //apply new viewpoint
    //document.getElementById('viewport').setAttribute('position', cam.pos);
    //document.getElementById('viewport').setAttribute('orientation', cam.rot);
    setView(extras.posMat, extras.rotMat);

    //clear any previous timeout
    if(typeof reenableViewpointListeningTimeout != 'undefined'){
  clearTimeout(reenableViewpointListeningTimeout);
    }
    //enable listening for changes in the viewport again some milliseconds after the last received msg
    reenableViewpointListeningTimeout = setTimeout(function(){processingMessage = false;} , 1000);
}

// Viewport section.
function sendPositionAndOrientation() {
  //only send if there is a role environment the viewer is embedded in
  if(!isEmbeddedInRole){
    return;
  }

  // Send through IWC!
  //var viewpointMsg = {'position': pos, 'orientation': rot}

  //send to wrapper
  //roleWrapper.postMessage("ViewpointUpdate " + JSON.stringify(viewpointMsg), "*");

  var view = getView();
  var viewpointMsg = {'posMat': view.posMat, 'rotMat': view.rotMat}
  roleWrapper.postMessage("ViewpointUpdate " + JSON.stringify(viewpointMsg), "*");
}

// Update position and rotation of the camera
function viewpointChanged(evt) {

  // Prevent widgets from sending updates again and again
  // If we set the position because we received a message we do not want to send it back
  if(!evt || processingMessage) {
    return;
  }

  // You can only send data once every 0.1 seconds.
  if(!canSend) {
    document.getElementById('debugText').innerHTML = "Bypassing send!";
    console.log("Bypassing send!");
    return;
  }
  canSend = false;
  if(typeof sendTimeout != 'undefined'){
    clearTimeout(sendTimeout);
  }
  sendTimeout = setTimeout(function(){
    canSend = true;
    document.getElementById('debugText').innerHTML = "Can send again!";
    console.log("Can send again!");
  } , 250);

  //printPositionAndOrientation('Updated', evt.position, evt.orientation);
  sendPositionAndOrientation();

  var view = getView();
  var intent = {
    'component' :'',                            // recipient, empty for broadcast
    'data'      :'http://data.org/some/data',   // data as URI
    'dataType'  :'text/xml',                    // data mime type
    'action'    :'ACTION_UPDATE',               // action to be performed by receivers
    'flags'     :['PUBLISH_GLOBAL'],            // control flags
    'extras'    :{'posMat': view.posMat,        // optional auxiliary data
                  'rotMat': view.rotMat},
  }

  console.info("subsite: intent: ", intent);
}

// Converts the position and orientation into a string and updates the view to display it.
function printPositionAndOrientation(str, pos, rot) {
  var camPos = pos.x.toFixed(4) + ' ' + pos.y.toFixed(4) + ' ' + pos.z.toFixed(4);
  var camRot = rot[0].x.toFixed(4) + ' ' + rot[0].y.toFixed(4) + ' '
                + rot[0].z.toFixed(4) + ' ' + rot[1].toFixed(4);
  document.getElementById('debugText').innerHTML = str + ' viewpoint position = ' +
                                                    camPos + ' orientation = ' + camRot;

  return {'pos': camPos, 'rot': camRot};
}

function showAllQuick(runtime, axis) {
  // If axis is not given, assume negative Z.
  if (axis === undefined)
    axis = "negZ";

  // Get the X3D scene element from runtime.
  var scene = runtime.canvas.doc._viewarea._scene;
  // Update it to latest boundary information.
  scene.updateVolume();
  // Record minimum and maximum positions (X,Y,Z) of the scene.
  var min = x3dom.fields.SFVec3f.copy(scene._lastMin);
  var max = x3dom.fields.SFVec3f.copy(scene._lastMax);
  // Get the scene main camera.
  var viewpoint = scene.getViewpoint();

  // Target forward axis for camera.
  var forwardAxis = new x3dom.fields.SFVec3f(0, 0, -1);

  // The coordinate system for camera.
  var x = "x", y = "y", z = "z";
  var sign = 1;

  // Setup the axis of rotation and coordinate system according to axis parameter.
  switch (axis) {

      case "posX":
      sign = -1;
      case "negX":

      x = "y";
      y = "z";
      z = "x";
      forwardAxis = new x3dom.fields.SFVec3f(sign, 0, 0);
      break;

      case "posY":
      sign = -1;
      case "negY":

      x = "z";
      y = "x";
      z = "y";
      forwardAxis = new x3dom.fields.SFVec3f(0, sign, 0);
      break;

      case "posZ":
      sign = -1;
      case "negZ":
      default:

      forwardAxis = new x3dom.fields.SFVec3f(0, 0, -sign);
      break;
  }

  // Get the field of view of camera.
  var fov = viewpoint.getFieldOfView();
  // Calculate boundary extents of scene.
  var extents = max.subtract(min);
  // Calculate the x and y spans of the camera.
  var denom = Math.tan(fov / 2.0);
  var offset = extents[z] / 2.0;
  var ySpan = (extents[y] / 2.0) / denom + offset;
  var xSpan = (extents[x] / 2.0) / denom + offset;
  // Offset the extents to the center.
  extents = min.add(extents.multiply(0.5));
  // Calculate and add the necessary Z to span all of scene.
  extents[z] += sign * (ySpan > xSpan ? ySpan : xSpan) * 1.01;

  // Setup translation matrix from extents.
  var translationMatrix = x3dom.fields.SFMatrix4f.translation(extents.negate());
  // Setup rotation matrix from axis of rotation.
  var rotationMatrix = x3dom.fields.Quaternion.rotateFromTo(new x3dom.fields.SFVec3f(0, 0, -1), forwardAxis).toMatrix();
  // Calculate final view matrix.
  var viewMatrix = rotationMatrix.mult(translationMatrix);

  // Set scene camera's matrix to the result.
  runtime.canvas.doc._viewarea._scene.getViewpoint().setView(viewMatrix);
}

function getView() {
  var runtime = document.getElementById('viewer_object').runtime;
  var posMat = runtime.canvas.doc._viewarea._transMat;
  var rotMat = runtime.canvas.doc._viewarea._rotMat;
  return {'posMat': posMat, 'rotMat': rotMat};
}
function setView(posMat, rotMat) {
  var runtime = document.getElementById('viewer_object').runtime;
  runtime.canvas.doc._viewarea._transMat.setValues(posMat);
  runtime.canvas.doc._viewarea._rotMat.setValues(rotMat);
  runtime.canvas.doc._viewarea._needNavigationMatrixUpdate  = true;
  runtime.canvas.doc.needRender = true;
}

function viewerMouseUp(event) {
  //printPositionAndOrientation('Sent final', getViewMatrix());
  sendPositionAndOrientation();

  var view = getView();
  var intent = {
    'component' :'',                            // recipient, empty for broadcast
    'data'      :'http://data.org/some/data',   // data as URI
    'dataType'  :'text/xml',                    // data mime type
    'action'    :'ACTION_UPDATE',               // action to be performed by receivers
    'flags'     :['PUBLISH_GLOBAL'],            // control flags
    'extras'    :{'posMat': view.posMat,        // optional auxiliary data
                  'rotMat': view.rotMat},
  }

  console.info("subsite: intent: ", intent);
}<|endoftext|>


<commit_before>// Karma configuration
// http://karma-runner.github.io/0.12/config/configuration-file.html
// Generated on 2015-07-31 using
// generator-karma 1.0.0

var puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();

module.exports = function (config) {
  'use strict';

  if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
    console.error('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.');
  }

  // Browsers to run on Sauce Labs
  // Check out https://saucelabs.com/platforms for all browser/OS combos
  var customLaunchers = {
    'sl_macOS_High_Sierra_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Chrome'
    },
    'sl_macOS_High_Sierra_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Safari'
    },
    'sl_macOS_High_Sierra_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Firefox'
    },
    'sl_macOS_Sierra_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Chrome'
    },
    'sl_macOS_Sierra_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Safari'
    },
    'sl_macOS_Sierra_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Firefox'
    },
    'sl_OSX_El_Capitan_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Chrome'
    },
    'sl_OSX_El_Capitan_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Safari'
    },
    'sl_OSX_El_Capitan_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Firefox'
    },
    'sl_Win10_Edge_13'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '13.10586'
    },
    'sl_Win10_Edge_14'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '14.14393'
    },
    'sl_Win10_Edge_15'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '15.15063'
    },
    'sl_Win10_Edge_16'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '16.16299'
    },
    'sl_Win10_Edge_17'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '17.17134'
    },
    'sl_Win10_IE11'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Internet Explorer',
      version     : '11.103'
    },
    'sl_Win81_IE11'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Internet Explorer',
      version     : '11.0'
    },
    'sl_Win8_IE10'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Internet Explorer',
      version     : '10.0'
    },
    'sl_Win7_IE9'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Internet Explorer',
      version     : '9.0'
    },
    'sl_Win10_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Chrome'
    },
    'sl_Win81_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Chrome'
    },
    'sl_Win8_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Chrome'
    },
    'sl_Win7_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Chrome'
    },
    'sl_Win10_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Firefox'
    },
    'sl_Win81_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Firefox'
    },
    'sl_Win8_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Firefox'
    },
    'sl_Win7_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Firefox'
    }
  };

  config.set({
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch : true,

    // base path, that will be used to resolve files and exclude
    basePath : '../',

    // testing framework to use (jasmine/mocha/qunit/...)
    // as well as any additional frameworks (requirejs/chai/sinon/...)
    frameworks : [
      "jasmine-jquery",
      "jasmine"
    ],

    // list of files / patterns to load in the browser
    files : [
      "lzwCompress.js",
      "test/mock/**/*.js",
      "test/spec/**/*.js",
      // fixtures
      {pattern : 'test/mock/*.json', watched : true, served : true, included : false}
    ],

    // list of files / patterns to exclude
    exclude : [],

    // web server port
    port : 8080,

    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers : Object.keys(customLaunchers),

    // Which plugins to enable
    plugins : [
      "karma-chrome-launcher",
      "karma-sauce-launcher",
      "karma-jasmine",
      "karma-jasmine-jquery",
      "karma-spec-reporter",
      "karma-coverage"
    ],

    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun : false,

    colors : true,

    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel : config.LOG_DEBUG,

    // Uncomment the following lines if you are using grunt's server to run the tests
    // proxies: {
    //   '/': 'http://localhost:9000/'
    // },
    // URL root prevent conflicts with the site root
    // urlRoot: '_karma_'

    // coverage reporter generates the coverage
    reporters : ['spec', 'coverage', 'saucelabs'],

    specReporter : {maxLogLines : 10},

    preprocessors : {
      // source files, that you wanna generate coverage for
      // do not include tests or libraries
      // (these files will be instrumented by Istanbul)
      'lzwCompress.js' : ['coverage']
    },

    // Sauce Labs config
    sauceLabs       : {
      testName          : 'lzwCompress.js on Sauce Labs',
      recordScreenshots : false,
      connectOptions    : {
        port    : 5757,
        logfile : 'sauce_connect.log'
      },
      public            : 'public'
    },
    // Increase timeout in case connection in CI is slow
    captureTimeout  : 120000,
    customLaunchers : customLaunchers,

    // configure the reporter
    coverageReporter : {
      dir       : 'coverage/',
      reporters : [
        // reporters not supporting the `file` property
        {type : 'html', subdir : 'report-html'},
        {type : 'lcov', subdir : 'report-lcov'},
        // reporters supporting the `file` property, use `subdir` to directly
        // output them in the `dir` directory
        {type : 'cobertura', subdir : '.', file : 'cobertura.txt'},
        {type : 'lcovonly', subdir : '.', file : 'report-lcovonly.txt'},
        {type : 'teamcity', subdir : '.', file : 'teamcity.txt'},
        {type : 'text', subdir : '.', file : 'text.txt'},
        {type : 'text-summary', subdir : '.', file : 'text-summary.txt'}
      ]
    }
  });
};
<commit_msg>trying to fix the saucelabs tests<commit_after>// Karma configuration
// http://karma-runner.github.io/0.12/config/configuration-file.html
// Generated on 2015-07-31 using
// generator-karma 1.0.0

var puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();

module.exports = function (config) {
  'use strict';

  if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
    console.error('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.');
  }

  // Browsers to run on Sauce Labs
  // Check out https://saucelabs.com/platforms for all browser/OS combos
  var customLaunchers = {
    'sl_macOS_High_Sierra_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Chrome'
    },
    'sl_macOS_High_Sierra_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Safari'
    },
    'sl_macOS_High_Sierra_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.13',
      browserName : 'Firefox'
    },
    'sl_macOS_Sierra_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Chrome'
    },
    'sl_macOS_Sierra_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Safari'
    },
    'sl_macOS_Sierra_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'macOS 10.12',
      browserName : 'Firefox'
    },
    'sl_OSX_El_Capitan_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Chrome'
    },
    'sl_OSX_El_Capitan_Safari'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Safari'
    },
    'sl_OSX_El_Capitan_Firefox'      : {
      base        : 'SauceLabs',
      platform    : 'OS X 10.11',
      browserName : 'Firefox'
    },
    'sl_Win10_Edge_13'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '13.10586'
    },
    'sl_Win10_Edge_14'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '14.14393'
    },
    'sl_Win10_Edge_15'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '15.15063'
    },
    'sl_Win10_Edge_16'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '16.16299'
    },
    'sl_Win10_Edge_17'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'MicrosoftEdge',
      version : '17.17134'
    },
    'sl_Win10_IE11'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Internet Explorer',
      version     : '11.103'
    },
    'sl_Win81_IE11'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Internet Explorer',
      version     : '11.0'
    },
    'sl_Win8_IE10'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Internet Explorer',
      version     : '10.0'
    },
    'sl_Win7_IE9'        : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Internet Explorer',
      version     : '9.0'
    },
    'sl_Win10_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Chrome'
    },
    'sl_Win81_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Chrome'
    },
    'sl_Win8_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Chrome'
    },
    'sl_Win7_Chrome'      : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Chrome'
    },
    'sl_Win10_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 10',
      browserName : 'Firefox'
    },
    'sl_Win81_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 8.1',
      browserName : 'Firefox'
    },
    'sl_Win8_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 8',
      browserName : 'Firefox'
    },
    'sl_Win7_Firefox'     : {
      base        : 'SauceLabs',
      platform    : 'Windows 7',
      browserName : 'Firefox'
    }
  };

  config.set({
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch : true,

    // base path, that will be used to resolve files and exclude
    basePath : '../',

    // testing framework to use (jasmine/mocha/qunit/...)
    // as well as any additional frameworks (requirejs/chai/sinon/...)
    frameworks : [
      "jasmine-jquery",
      "jasmine"
    ],

    // list of files / patterns to load in the browser
    files : [
      "lzwCompress.js",
      "test/mock/**/*.js",
      "test/spec/**/*.js",
      // fixtures
      {pattern : 'test/mock/*.json', watched : true, served : true, included : false}
    ],

    // list of files / patterns to exclude
    exclude : [],

    // web server port
    port : 8080,

    // Start these browsers, currently available:
    // - Chrome
    // - ChromeCanary
    // - Firefox
    // - Opera
    // - Safari (only Mac)
    // - PhantomJS
    // - IE (only Windows)
    browsers : Object.keys(customLaunchers),

    // Which plugins to enable
    plugins : [
      "karma-chrome-launcher",
      "karma-sauce-launcher",
      "karma-jasmine",
      "karma-jasmine-jquery",
      "karma-spec-reporter",
      "karma-coverage"
    ],

    // Continuous Integration mode
    // if true, it capture browsers, run tests and exit
    singleRun : false,

    colors : true,

    // level of logging
    // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
    logLevel : config.LOG_DEBUG,

    // Uncomment the following lines if you are using grunt's server to run the tests
    // proxies: {
    //   '/': 'http://localhost:9000/'
    // },
    // URL root prevent conflicts with the site root
    // urlRoot: '_karma_'

    // coverage reporter generates the coverage
    reporters : ['spec', 'coverage', 'saucelabs'],

    specReporter : {maxLogLines : 10},

    preprocessors : {
      // source files, that you wanna generate coverage for
      // do not include tests or libraries
      // (these files will be instrumented by Istanbul)
      'lzwCompress.js' : ['coverage']
    },

    // Sauce Labs config
    sauceLabs       : {
      testName          : 'lzwCompress.js on Sauce Labs',
      recordScreenshots : false,
      public            : 'public'
    },
    // Increase timeout in case connection in CI is slow
    captureTimeout  : 120000,
    customLaunchers : customLaunchers,

    // configure the reporter
    coverageReporter : {
      dir       : 'coverage/',
      reporters : [
        // reporters not supporting the `file` property
        {type : 'html', subdir : 'report-html'},
        {type : 'lcov', subdir : 'report-lcov'},
        // reporters supporting the `file` property, use `subdir` to directly
        // output them in the `dir` directory
        {type : 'cobertura', subdir : '.', file : 'cobertura.txt'},
        {type : 'lcovonly', subdir : '.', file : 'report-lcovonly.txt'},
        {type : 'teamcity', subdir : '.', file : 'teamcity.txt'},
        {type : 'text', subdir : '.', file : 'text.txt'},
        {type : 'text-summary', subdir : '.', file : 'text-summary.txt'}
      ]
    }
  });
};
<|endoftext|>


<commit_before>import RendererPipeline from './pipeline';

function domImgToCanvas(img) {
  const fullresCanvas = document.createElement('canvas');
  const fullresContext = fullresCanvas.getContext('2d');
  fullresCanvas.width = img.naturalWidth;
  fullresCanvas.height = img.naturalHeight;
  // flipped y-axis
  fullresContext.translate(0, img.naturalHeight);
  fullresContext.scale(1, -1);
  fullresContext.drawImage(img, 0, 0);
  return fullresCanvas;
}

function mapImageToParticles(imageCanvas, config) {
  const w = config.xParticlesCount;
  const h = config.yParticlesCount;
  if (w < 1 || h < 1) {
    throw new Error('Illegal values for particle counts: x=' + w + ', y=' + h);
  }
  const dAspectRatio = w / h;
  const sAspectRatio = imageCanvas.width / imageCanvas.height;
  const scalingCanvas = document.createElement('canvas');
  const scalingContext = scalingCanvas.getContext('2d');
  let sx = 0;
  let sy = 0;
  let sWidth = imageCanvas.width;
  let sHeight = imageCanvas.height;
  let dx = 0;
  let dy = 0;
  let dWidth = w;
  let dHeight = h;
  switch (config.imageScaling) {
    case 'crop-to-viewport': {
      break;
    }
    case 'fit-image': {
      break;
    }
    case 'fit-width': {
      break;
    }
    case 'fit-height': {
      break;
    }
    case 'scale-to-viewport': {
      // Nothing to do. Default values are already the ones we need.
      break;
    }
    default:
      throw new Error('Illegal value for config.imageScaling: ' + config.imageScaling);
  }
  switch(config.imageCropping.x) {
    case 'crop-both': {
      break;
    }
    case 'crop-left': {
      break;
    }
    case 'crop-right': {
      break;
    }
    default:
      throw new Error('Illegal value for config.imageCropping.x: ' + config.imageCropping.x);
  }
  switch(config.imageCropping.y) {
    case 'crop-both': {
      break;
    }
    case 'crop-top': {
      break;
    }
    case 'crop-bottom': {
      break;
    }
    default:
      throw new Error('Illegal value for config.imageCropping.x: ' + config.imageCropping.x);
  }
  scalingCanvas.width = w;
  scalingCanvas.height = h;
  scalingContext.drawImage(imageCanvas, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
  return scalingContext.getImageData(0, 0, scalingCanvas.width, scalingCanvas.height);
}

class ParticleData {
  constructor(imageData, regl, config) {
    const w = config.xParticlesCount || imageData.width;
    const h = config.yParticlesCount || imageData.height;
    this.destroyed = false;

    const scaledData = mapImageToParticles(imageData, config);

    const particlePixels = scaledData.data;

    const pixelIndices = Array.from(Array(w * h).keys());

    const texcoords = pixelIndices.map((i) => [((i % w) + 0.5) / w, (Math.floor(i / w) + 0.5) / h]);

    const rgb = pixelIndices.map((i) => {
      const pixel = particlePixels.slice(i * 4, (i * 4) + 4);

      return [pixel[0] / 255, pixel[1] / 255, pixel[2] / 255];
    });

    const hsv = pixelIndices.map((i) => {
      const pixel = rgb[i];

      const cMax = Math.max(pixel[0], pixel[1], pixel[2]);
      const cMin = Math.min(pixel[0], pixel[1], pixel[2]);
      const d = cMax - cMin;

      if (d < 0.00001 || cMax < 0.00001) {
        return [0, 0, cMax];
      }

      let _h;
      if (cMax === pixel[0]) {
        _h = (pixel[1] - pixel[2]) / d;
        if (_h < 0) {
          _h += 6;
        }
      } else if (cMax === pixel[1]) {
        _h = ((pixel[2] - pixel[0]) / d) + 2;
      } else {
        _h = ((pixel[0] - pixel[1]) / d) + 4;
      }

      return [_h * 60 * (Math.PI / 180), d / cMax, cMax];
    });
    this.width           = w;
    this.height          = h;
    this.texcoordsBuffer = regl.buffer(texcoords);
    this.rgbBuffer       = regl.buffer(rgb);
    this.hsvBuffer       = regl.buffer(hsv);
  }
  destroy() {
    if (!this.destroyed) {
      this.texcoordsBuffer.destroy();
      this.rgbBuffer.destroy();
      this.hsvBuffer.destroy();
      this.destroyed = true;
    }
  }
}

/**
 * Encapsulates the parts of the render pipeline which are subject to
 * dynamic change, i.e. data that can be changed by effects.
 *
 * In contrast to this, data inside a `config` object is always immutable
 * (as long as the user does not request changes to be applied - which
 * generates a new `config` object).
 * The most important thing to note is that both `state` *and* `config`
 * objects "live on" if the other object is changed, whereas only `state`
 * is ever influenced by `config` - never the other way around.
 * E.g. config's xParticleCount influences state's particleData.
 * On the other hand, `state` does not need to be serializable
 */
export default class RendererState {
  constructor(regl) {
    this.regl = regl;
    this.pipeline = new RendererPipeline(regl);

    // Properties
    this.config = null;
    this.particleData = -1;
    this.particleDataStore = [[null, null]];
    this.buffers = [];
    this.hooks = [];
    this.width = 0;
    this.height = 0;
  }
  adaptToConfig(config) {
    this.config = config;
    this.pipeline.reset(config.backgroundColor);

    // Update default particle data
    const defaultImg = this.particleDataStore[0][0];
    if (defaultImg !== null) {
      const defaultParticleData = this.particleDataStore[0][1];
      if (defaultParticleData !== null) {
        defaultParticleData.destroy();
      }
      this.particleDataStore[0][1] = new ParticleData(
        defaultImg,
        this.regl,
        config
      );
    }
    // release resources
    for (let i = 1; i < this.particleDataStore.length; i++) {
      this.destroyParticleData(i);
    }
    this.particleDataStore.length = 1;
    this.particleData = 0;

    for (let i = 0; i < this.buffers.length; i++) {
      this.buffers[i].destroy();
    }
    this.buffers.length = 0;
    // run hooks
    for (let i = 0; i < this.hooks.length; i++) {
      this.hooks[i]();
    }
  }
  setParticleData(id) {
    this.particleData = id;
  }
  createParticleData(imgData) {
    this.particleDataStore.push([
      imgData,
      new ParticleData(
        imgData,
        this.regl,
        this.config
      )
    ]);
    return this.particleDataStore.length - 1;
  }
  createParticleDataFromDomImg(domImg) {
    return this.createParticleData(domImgToCanvas(domImg));
  }
  destroyParticleData(id) {
    if (this.particleDataStore[id][1]) {
      this.particleDataStore[id][1].destroy();
      this.particleDataStore[id] = [null, null];
    }
  }
  getCurrentParticleData() {
    if (this.particleData < 0) {
      return null;
    }
    return this.particleDataStore[this.particleData][1];
  }
  createBuffer(...args) {
    const buf = this.regl.buffer(...args);
    this.buffers.push(buf);
    return { id: this.buffers.length - 1, buffer: buf };
  }
  destroyBuffer(id) {
    if (id < 0 || id >= this.buffers.length) {
      throw new Error('Illegal buffer id given for destruction');
    }
    this.buffers[id].destroy();
    this.buffers.splice(id, 1);
  }
  isValid() {
    return this.particleData >= 0 && this.pipeline.isValid();
  }
  setDefaultDomImage(domImage) {
    this.particleDataStore[0][0] = domImgToCanvas(domImage);
    this.particleData = 0;
  }
  /// Hooks are run after the state has adapted to a new config object
  addHook(hook) {
    this.hooks.push(hook);
  }
  /// Changes the viewport dimension
  /// Not to be confused with the particle grid size. See
  /// config.xParticlesCount and config.yParticlesCount for that
  resize(width, height) {
    this.width = width;
    this.height = height;
    this.pipeline.resize(width, height);
  }
  /// @return viewport width
  getWidth() {
    return this.width;
  }
  /// @return viewport height
  getHeight() {
    return this.height;
  }
}
<commit_msg>Fully implement mapImageToParticles<commit_after>import RendererPipeline from './pipeline';

function domImgToCanvas(img) {
  const fullresCanvas = document.createElement('canvas');
  const fullresContext = fullresCanvas.getContext('2d');
  fullresCanvas.width = img.naturalWidth;
  fullresCanvas.height = img.naturalHeight;
  // flipped y-axis
  fullresContext.translate(0, img.naturalHeight);
  fullresContext.scale(1, -1);
  fullresContext.drawImage(img, 0, 0);
  return fullresCanvas;
}

class ScalingInfo {
  constructor(particleCounts, imageScaling, imageCropping, viewport) {
    this.particleCounts = particleCounts; // {x, y}
    this.imageScaling = imageScaling;
    this.imageCropping = imageCropping; // {x, y}
    this.viewport = viewport; // {width, height}
  }
}

function getDefaultPixelParticleMappingParams(imageCanvas, scalingInfo) {
  const w = scalingInfo.particleCounts.x;
  const h = scalingInfo.particleCounts.y;
  const r = {
    sx: 0,
    sy: 0,
    sWidth: imageCanvas.width,
    sHeight: imageCanvas.height,
    dx: 0,
    dy: 0,
    dWidth: w,
    dHeight: h,
  };
  r.dAspectRatio = (w / h) * (scalingInfo.viewport.width / scalingInfo.viewport.height);
  r.sAspectRatio = imageCanvas.width / imageCanvas.height;
  return r;
}

function getCropImageToViewportParams(imageCanvas, scalingInfo) {
  const r = getDefaultPixelParticleMappingParams(imageCanvas, scalingInfo);
  if (r.dAspectRatio > r.sAspectRatio) { // source height will exceed dest height
    r.sHeight = r.sWidth / r.dAspectRatio;
    if (scalingInfo.imageCropping.y === 'crop-both') {
      r.sy = (imageCanvas.height - r.sHeight) / 2;
    } else if (scalingInfo.imageCropping.y === 'crop-top') {
      r.sy = imageCanvas.height - r.sHeight;
    } else if (scalingInfo.imageCropping.y === 'crop-bottom') {
      r.sy = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.y: ' + scalingInfo.imageCropping.x);
    }
  } else { // source width will exceed dest width
    r.sWidth = r.sHeight * r.dAspectRatio;
    if (scalingInfo.imageCropping.x === 'crop-both') {
      r.sx = (imageCanvas.width - r.sWidth) / 2;
    } else if (scalingInfo.imageCropping.x === 'crop-left') {
      r.sx = imageCanvas.width - r.sWidth;
    } else if (scalingInfo.imageCropping.x === 'crop-right') {
      r.sx = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.x: ' + scalingInfo.imageCropping.x);
    }
  }
  return r;
}

function getFitWidthParams(imageCanvas, scalingInfo) {
  const w = scalingInfo.particleCounts.x;
  const h = scalingInfo.particleCounts.y;
  const r = getDefaultPixelParticleMappingParams(imageCanvas, scalingInfo);
  if (r.dAspectRatio < r.sAspectRatio) { // the picture won't fill the particles. Some rows will remain black
    r.dHeight = w / r.sAspectRatio;
    if (scalingInfo.imageCropping.y === 'crop-both') {
      r.dy = (h - r.dHeight) / 2;
    } else if (scalingInfo.imageCropping.y === 'crop-top') {
      r.dy = h - r.dHeight;
    } else if (scalingInfo.imageCropping.y === 'crop-bottom') {
      r.dy = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.y: ' + scalingInfo.imageCropping.y);
    }
  } else { // pixels rows at the top and/or bottom will need to be discarded
    r.sHeight = imageCanvas.width / r.dAspectRatio;
    if (scalingInfo.imageCropping.y === 'crop-both') {
      r.sy = (imageCanvas.height - r.sHeight) / 2;
    } else if (scalingInfo.imageCropping.y === 'crop-top') {
      r.sy = imageCanvas.height  - r.sHeight;
    } else if (scalingInfo.imageCropping.y === 'crop-bottom') {
      r.sy = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.y: ' + scalingInfo.imageCropping.y);
    }
  }
  return r;
}

function getFitHeightParams(imageCanvas, scalingInfo) {
  const w = scalingInfo.particleCounts.x;
  const h = scalingInfo.particleCounts.y;
  const r = getDefaultPixelParticleMappingParams(imageCanvas, scalingInfo);
  if (r.dAspectRatio < r.sAspectRatio) { // the picture won't fill the particles. Some columns will remain black
    r.dWidth = h * r.sAspectRatio;
    if (scalingInfo.imageCropping.x === 'crop-both') {
      r.dx = (w - r.dWidth) / 2;
    } else if (scalingInfo.imageCropping.x === 'crop-left') {
      r.dx = w - r.dWidth;
    } else if (scalingInfo.imageCropping.x === 'crop-right') {
      r.dx = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.x: ' + scalingInfo.imageCropping.x);
    }
  } else { // pixels columns to the left and/or right will need to be discarded
    r.sWidth = imageCanvas.height * r.dAspectRatio;
    if (scalingInfo.imageCropping.x === 'crop-both') {
      r.sx = (imageCanvas.width - r.sWidth) / 2;
    } else if (scalingInfo.imageCropping.x === 'crop-left') {
      r.sx = imageCanvas.width - r.sWidth;
    } else if (scalingInfo.imageCropping.x === 'crop-right') {
      r.sx = 0;
    } else {
      throw new Error('Illegal value for scalingInfo.imageCropping.x: ' + scalingInfo.imageCropping.x);
    }
  }
  return r;
}

function mapImageToParticles(imageCanvas, scalingInfo) {
  const w = scalingInfo.particleCounts.x;
  const h = scalingInfo.particleCounts.y;
  if (w < 1 || h < 1) {
    throw new Error('Illegal values for particle counts: x=' + w + ', y=' + h);
  }
  const scalingCanvas = document.createElement('canvas');
  const scalingContext = scalingCanvas.getContext('2d');
  let scalingParams = null;
  if (scalingInfo.imageScaling === 'crop-to-viewport') {
    scalingParams = getCropImageToViewportParams(imageCanvas, scalingInfo);
  } else if (scalingInfo.imageScaling === 'fit-image') {
    const dAspectRatio = (w / h) * (scalingInfo.viewport.width / scalingInfo.viewport.height);
    if (imageCanvas.width / imageCanvas.height > dAspectRatio) {
      scalingParams = getFitWidthParams(imageCanvas, scalingInfo);
    } else {
      scalingParams = getFitHeightParams(imageCanvas, scalingInfo);
    }
  } else if (scalingInfo.imageScaling === 'fit-width') {
    scalingParams = getFitWidthParams(imageCanvas, scalingInfo);
  } else if (scalingInfo.imageScaling === 'fit-height') {
    scalingParams = getFitHeightParams(imageCanvas, scalingInfo);
  } else if (scalingInfo.imageScaling === 'scale-to-viewport') {
    scalingParams = getDefaultPixelParticleMappingParams();
  } else {
    throw new Error('Illegal value for scalingInfo.imageScaling: ' + scalingInfo.imageScaling);
  }
  scalingCanvas.width = w;
  scalingCanvas.height = h;
  const P = scalingParams;
  scalingContext.drawImage(imageCanvas, P.sx, P.sy, P.sWidth, P.sHeight, P.dx, P.dy, P.dWidth, P.dHeight);
  return scalingContext.getImageData(0, 0, scalingCanvas.width, scalingCanvas.height);
}

class ParticleData {
  constructor(imageData, regl, scalingInfo) {
    const w = scalingInfo.particleCounts.x || imageData.width;
    const h = scalingInfo.particleCounts.y || imageData.height;
    this.destroyed = false;

    const scaledData = mapImageToParticles(imageData, scalingInfo);

    const particlePixels = scaledData.data;

    const pixelIndices = Array.from(Array(w * h).keys());

    const texcoords = pixelIndices.map((i) => [((i % w) + 0.5) / w, (Math.floor(i / w) + 0.5) / h]);

    const rgb = pixelIndices.map((i) => {
      const pixel = particlePixels.slice(i * 4, (i * 4) + 4);

      return [pixel[0] / 255, pixel[1] / 255, pixel[2] / 255];
    });

    const hsv = pixelIndices.map((i) => {
      const pixel = rgb[i];

      const cMax = Math.max(pixel[0], pixel[1], pixel[2]);
      const cMin = Math.min(pixel[0], pixel[1], pixel[2]);
      const d = cMax - cMin;

      if (d < 0.00001 || cMax < 0.00001) {
        return [0, 0, cMax];
      }

      let _h;
      if (cMax === pixel[0]) {
        _h = (pixel[1] - pixel[2]) / d;
        if (_h < 0) {
          _h += 6;
        }
      } else if (cMax === pixel[1]) {
        _h = ((pixel[2] - pixel[0]) / d) + 2;
      } else {
        _h = ((pixel[0] - pixel[1]) / d) + 4;
      }

      return [_h * 60 * (Math.PI / 180), d / cMax, cMax];
    });
    this.width           = w;
    this.height          = h;
    this.texcoordsBuffer = regl.buffer(texcoords);
    this.rgbBuffer       = regl.buffer(rgb);
    this.hsvBuffer       = regl.buffer(hsv);
  }
  destroy() {
    if (!this.destroyed) {
      this.texcoordsBuffer.destroy();
      this.rgbBuffer.destroy();
      this.hsvBuffer.destroy();
      this.destroyed = true;
    }
  }
}

/**
 * Encapsulates the parts of the render pipeline which are subject to
 * dynamic change, i.e. data that can be changed by effects.
 *
 * In contrast to this, data inside a `config` object is always immutable
 * (as long as the user does not request changes to be applied - which
 * generates a new `config` object).
 * The most important thing to note is that both `state` *and* `config`
 * objects "live on" if the other object is changed, whereas only `state`
 * is ever influenced by `config` - never the other way around.
 * E.g. config's xParticleCount influences state's particleData.
 * On the other hand, `state` does not need to be serializable
 */
export default class RendererState {
  constructor(regl) {
    this.regl = regl;
    this.pipeline = new RendererPipeline(regl);

    // Properties
    this.config = null;
    this.particleData = -1;
    this.particleDataStore = [[null, null]];
    this.buffers = [];
    this.hooks = [];
    this.width = 0;
    this.height = 0;
  }
  adaptToConfig(config) {
    this.config = config;
    this.pipeline.reset(config.backgroundColor);

    // Update default particle data
    const defaultImg = this.particleDataStore[0][0];
    if (defaultImg !== null) {
      const defaultParticleData = this.particleDataStore[0][1];
      if (defaultParticleData !== null) {
        defaultParticleData.destroy();
      }
      const scalingInfo = new ScalingInfo(
        {x: config.xParticlesCount, y: config.yParticlesCount},
        config.imageScaling, config.imageCropping,
        {width: this.getWidth(), height: this.getHeight()}
      );
      this.particleDataStore[0][1] = new ParticleData(
        defaultImg,
        this.regl,
        scalingInfo
      );
    }
    // release resources
    for (let i = 1; i < this.particleDataStore.length; i++) {
      this.destroyParticleData(i);
    }
    this.particleDataStore.length = 1;
    this.particleData = 0;

    for (let i = 0; i < this.buffers.length; i++) {
      this.buffers[i].destroy();
    }
    this.buffers.length = 0;
    // run hooks
    for (let i = 0; i < this.hooks.length; i++) {
      this.hooks[i]();
    }
  }
  setParticleData(id) {
    this.particleData = id;
  }
  createParticleData(imgData) {
    const scalingInfo = new ScalingInfo(
      {x: this.config.xParticlesCount, y: this.config.yParticlesCount},
      this.config.imageScaling, this.config.imageCropping,
      {width: this.getWidth(), height: this.getHeight()}
    );
    this.particleDataStore.push([
      imgData,
      new ParticleData(
        imgData,
        this.regl,
        scalingInfo
      )
    ]);
    return this.particleDataStore.length - 1;
  }
  createParticleDataFromDomImg(domImg) {
    return this.createParticleData(domImgToCanvas(domImg));
  }
  destroyParticleData(id) {
    if (this.particleDataStore[id][1]) {
      this.particleDataStore[id][1].destroy();
      this.particleDataStore[id] = [null, null];
    }
  }
  getCurrentParticleData() {
    if (this.particleData < 0) {
      return null;
    }
    return this.particleDataStore[this.particleData][1];
  }
  createBuffer(...args) {
    const buf = this.regl.buffer(...args);
    this.buffers.push(buf);
    return { id: this.buffers.length - 1, buffer: buf };
  }
  destroyBuffer(id) {
    if (id < 0 || id >= this.buffers.length) {
      throw new Error('Illegal buffer id given for destruction');
    }
    this.buffers[id].destroy();
    this.buffers.splice(id, 1);
  }
  isValid() {
    return this.particleData >= 0 && this.pipeline.isValid();
  }
  setDefaultDomImage(domImage) {
    this.particleDataStore[0][0] = domImgToCanvas(domImage);
    this.particleData = 0;
  }
  /// Hooks are run after the state has adapted to a new config object
  addHook(hook) {
    this.hooks.push(hook);
  }
  /// Changes the viewport dimension
  /// Not to be confused with the particle grid size. See
  /// config.xParticlesCount and config.yParticlesCount for that
  resize(width, height) {
    this.width = width;
    this.height = height;
    this.pipeline.resize(width, height);
  }
  /// @return viewport width
  getWidth() {
    return this.width;
  }
  /// @return viewport height
  getHeight() {
    return this.height;
  }
}
<|endoftext|>


<commit_before>import test from 'ava';
import gtr from '.';

const https = 'https://github.com/sh0ji/focus-rover.git';
const invalid = 'github.com/sh0ji/focus-rover';

test('public https get', async (t) => {
	const tags = await gtr.get(https);
	t.is(tags.get('v1.0.0'), '100b2a539ae13406d70a2062bde8d6e4e329fa87');
	t.is(tags.get('v2.0.0'), '7480367b30525107d89aac5ae08cfec991a4e933');
});

test('public https latest', async (t) => {
	const latest = await gtr.latest(https);
	t.is(latest[0], 'v2.0.4');
	t.is(latest[1], '3b73969ca3a02047e2925d7bbe834da947a272e1');
});

test('invalid url', async (t) => {
	const error = await t.throwsAsync(gtr.get(invalid));
	t.is(
		error.message,
		`fatal: '${invalid}' does not appear to be a git repository\n`
		+ 'fatal: Could not read from remote repository.\n\n'
		+ 'Please make sure you have the correct access rights\n'
		+ 'and the repository exists.\n',
	);
});
<commit_msg>test: update latest tag<commit_after>import test from 'ava';
import gtr from '.';

const https = 'https://github.com/sh0ji/focus-rover.git';
const invalid = 'github.com/sh0ji/focus-rover';

test('public https get', async (t) => {
	const tags = await gtr.get(https);
	t.is(tags.get('v1.0.0'), '100b2a539ae13406d70a2062bde8d6e4e329fa87');
	t.is(tags.get('v2.0.0'), '7480367b30525107d89aac5ae08cfec991a4e933');
});

test('public https latest', async (t) => {
	const latest = await gtr.latest(https);
	t.is(latest[0], 'v2.0.5');
	t.is(latest[1], '722b2bef2d95d2a1ae6381d67dcf114f6fb48656');
});

test('invalid url', async (t) => {
	const error = await t.throwsAsync(gtr.get(invalid));
	t.is(
		error.message,
		`fatal: '${invalid}' does not appear to be a git repository\n`
		+ 'fatal: Could not read from remote repository.\n\n'
		+ 'Please make sure you have the correct access rights\n'
		+ 'and the repository exists.\n',
	);
});
<|endoftext|>