class: center, middle, inverse, title-slide # Correspondence Analysis ## High Dimensional Data Analysis ### Anastasios Panagiotelis & Ruben Loaiza-Maya ### Lecture 11 --- class: inverse, center, middle # Motivation --- # Non-metric data - So far we have looked at dimension reduction methods such as PCA and MDS where:<!--D--> -- - The number of variables is large<!--D--> -- - The data are (mostly) metric data<!--D--> -- - Today we cover tools for understanding the relationships between nominal/categorical data.<!--D--> -- - We focus on the case where there are only two variables, but a potentially large number of categories for each variable. --- # Outline - First we revise the cross tabulation, a useful summary for nominal data.<!--D--> -- - We then cover ways to visualise the information in a cross tab.<!--D--> -- - Ultimately we will discuss Correspondence Analysis which can be applied to large tables.<!--D--> -- - We cover applications of Correspondence Analysis in the real world including with text data. --- class: inverse, center, middle # A basic analysis --- # Beer example - A cross tab can be created in R using the `table` function. The input is either<!--D--> -- - A single matrix or data frame with 2 columns/variables<!--D--> -- - One vector for each variable<!--D--> -- - The output is a *table* object.<!--D--> -- - Let’s try it with the Beer data which can be found on Moodle. --- # Beer example - We look at two categorical variables<!--D--> -- - Availabilty - Light<!--D--> -- - The number of categories for availability is 2 (National/Regional)<!--D--> -- - The number of categories for light is 2 (Light/non-light). --- # Doing it in R ```r load('Beer.RData') Beer %>% select(light,avail)%>% table%>% #Creates Tables addmargins()-> #Includes totals crosstab ``` --- # The table ```r print(crosstab) ``` ``` ## avail ## light National Regional Sum ## NONLIGHT 7 21 28 ## LIGHT 5 2 7 ## Sum 12 23 35 ``` --- # What do we see? - There are more beers available at a regional level.<!--D--> -- - The nationally available beers are just as likely to be light or non-light.<!--D--> -- - Regional beers are overwhelmingly non-light.<!--D--> -- - But is there a way we can visualise this? --- # How to visualise? - In this small example we can think of four sets of coordinates<!--D--> -- - Coordinates for national<!--D--> -- - Coordinates for regional<!--D--> -- - Coordinates for non-light<!--D--> -- - Coordinates for light<!--D--> -- - Let's plot these --- # Plot <img src="CA_files/figure-html/2dplot-1.png" style="display: block; margin: auto;" /> --- # Summary - Even on this very basic plot we can see an association between light beers and national availability<!--D--> -- - However what do we do with<!--D--> -- - Large cross tabulations <!--D--> -- - Non-square cross tabulations <!--D--> -- - To solve these issues **Correspondence Analysis** can be used. It is more complicated than simply plotting rows and columns in the cross tab --- class: inverse, center, middle # A Bigger Cross Tab --- # Breakfast example - The table on the next slide is reproduced from Bendixen, M., (2003).<!--D--> -- - Different *breakfast* foods (e.g. CER=cereal, MUE=muesli), with a total of 8 categories.<!--D--> -- - Different *attributes* of those foods (‘Healthy’, ‘Economical’, ‘Tasteless’) with a total of 14 categories.<!--D--> -- - Survey asked to match attributes to breakfasts.<!--D--> -- - A cross tab shows the frequency with which each food was matched to each attribute. --- # Breakfast example <div style="border: 1px solid #ddd; padding: 0px; overflow-y: scroll; height:500px; "><table class="table table-striped table-hover table-condensed" style="margin-left: auto; margin-right: auto;"> <thead> <tr> <th style="text-align:left;position: sticky; top:0; background-color: #FFFFFF;"> </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> BE </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> CER </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> FRF </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> MUE </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> POR </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> STF </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> TT </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> YOG </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> Economical </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 20 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 7 </td> </tr> <tr> <td style="text-align:left;"> Expensive </td> <td style="text-align:right;"> 27 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 33 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 10 </td> </tr> <tr> <td style="text-align:left;"> Family Favourite </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 5 </td> </tr> <tr> <td style="text-align:left;"> Healthy </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 38 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 34 </td> </tr> <tr> <td style="text-align:left;"> Long Prepare </td> <td style="text-align:right;"> 35 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> Nutritious </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 31 </td> </tr> <tr> <td style="text-align:left;"> Quick </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 54 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 33 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 20 </td> </tr> <tr> <td style="text-align:left;"> Summer </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 42 </td> <td style="text-align:right;"> 37 </td> <td style="text-align:right;"> 22 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 35 </td> </tr> <tr> <td style="text-align:left;"> Tasteless </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> </tr> <tr> <td style="text-align:left;"> Tasty </td> <td style="text-align:right;"> 34 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 33 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 26 </td> </tr> <tr> <td style="text-align:left;"> Treat </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 17 </td> </tr> <tr> <td style="text-align:left;"> Weekdays </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 47 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 10 </td> </tr> <tr> <td style="text-align:left;"> Weekends </td> <td style="text-align:right;"> 56 </td> <td style="text-align:right;"> 12 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 18 </td> </tr> <tr> <td style="text-align:left;"> Winter </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 8 </td> </tr> </tbody> </table></div> --- # Visualising this We can visualise this using Correspondence Analysis which requires the `ca` package. ```r library(ca) caout<-ca(breakfastct) plot(caout) ``` You need to install the `ca` package first --- # Visualising this ``` ## Warning: package 'ca' was built under R version 3.6.2 ``` <img src="CA_files/figure-html/breakfastcap-1.png" style="display: block; margin: auto;" /> --- # What can we see? - Towards the top of the plot are categories like *Expensive*, *Healthy* and *Nutritious*. There are associated with *Muesli (MUE)* and *Fresh Fruit(FRF)*.<!--D--> -- - The left of the plot has the catgeory *Long Prepare*, with *Bacon and Eggs (BE)* closest to this point.<!--D--> -- - *Cereal (CER)* is associated with *Weekdays*.<!--D--> -- - What else? --- # Correspondence Analysis - The plot is easy to interpret. Categories that are close to one another on the plot have a strong association with one another.<!--D--> -- - This is the case when we compare<!--D--> -- + Two categories in the rows of the table,<!--D--> -- + Two categories in the column of the table,<!--D--> -- + A category in the row of the cross tab with a category in the column of a cross tab<!--D--> -- - What about the remaining output? --- # Other output ```r summary(caout,row=FALSE,column=FALSE) ``` ``` ## ## Principal inertias (eigenvalues): ## ## dim value % cum% scree plot ## 1 0.193095 52.5 52.5 ************* ## 2 0.077731 21.1 73.6 ***** ## 3 0.043854 11.9 85.6 *** ## 4 0.032804 8.9 94.5 ** ## 5 0.012257 3.3 97.8 * ## 6 0.005687 1.5 99.4 ## 7 0.002363 0.6 100.0 ## -------- ----- ## Total: 0.367791 100.0 ``` --- # Connection to PCA/MDS - There are similarities with material covered in PCA and MDS <!--D--> -- - We visualise with a biplot. <!--D--> -- - Terms such as **eigenvalues** and **scree plot** reappear.<!--D--> -- - In PCA/MDS the aim was to maximise variance or minimise strain.<!--D--> -- - In CA the aim is to maximise **inertia**. --- # Inertia - Categorical data are not ordinal.<!--D--> -- - We cannot measure dependence in categorical data by seeing whether 'large' values of one variable coincide with 'large' values of the other variable.<!--D--> -- - We cannot use correlation.<!--D--> -- - Inertia is a measure of the dependence in categorical data, closely related to the chi square statistic from a test of independence between two categorical variables.<!--D--> -- - Let us quickly revise this. --- # Chi Square test - Suppose we have two variables<!--D--> -- + Variable 1 has two categories A and B<!--D--> -- + Variable 2 has two categories X and Y <!--D--> -- - Assume Variable 1 and 2 are independent<!--D--> -- - On the next slide we will have an incomplete cross tab --- # Cross Tab |V1 \ V2| X | Y | Total | |------:|:-----|---------|:------:| | A | | | 50 | | B | | | 50 | |Total | 20 | 80 | 100 | If variable 1 and variable 2 are independent then what numbers do you expect to be in the empty cells? --- # Cross Tab |V1 \ V2| X | Y | Total | |------:|:-----|---------|:------:| | A | 10 | 40 | 50 | | B | 10 | 40 | 50 | |Total | 20 | 80 | 100 | Under independence - `\(\mbox{Pr}(A,X)=\mbox{Pr}(A)\mbox{Pr}(X)\)` - `\(\mbox{Pr}(B,X)=\mbox{Pr}(B)\mbox{Pr}(X)\)` - `\(\mbox{Pr}(A,Y)=\mbox{Pr}(A)\mbox{Pr}(Y)\)` - `\(\mbox{Pr}(B,Y)=\mbox{Pr}(B)\mbox{Pr}(Y)\)` --- # Independence is boring - Independence is not interesting.<!--D--> -- - We cannot draw any conclusions about association between categories across different variables.<!--D--> -- - If we were to do the crude plot from the beer example, all points would lie in the same direction.<!--D--> -- - In correspondence analysis, for perfect independence all row and column categories fall on a single point. --- # Random variation - Even for independence, due to randomness we may actually get a table like this: |V1 \ V2| X | Y | Total | |------:|:-----|---------|:------:| | A | 12 | 38 | 50 | | B | 8 | 42 | 50 | |Total | 20 | 80 | 100 | - How do we know whether the variables are truly independent and not due to random variation? --- # The chi square test For the chi square test, in each cell we compute `$$\frac{(O_{ij}-E_{ij})^2}{E_{ij}}$$` where `\(O_{ij}\)` is the observed count in each cell and `\(E_{ij}\)` is the expected count in each cell. --- # Chi Square Statistic The chi square statistic is `$$\chi^2=\sum\limits_{i=1}^{r}\sum\limits_{j=1}^{c}\frac{(O_{ij}-E_{ij})^2}{E_{ij}}$$` where `\(r\)` and `\(c\)` are the number of rows and columns in the cross tab respectively. --- # The chi square test - If the variables are truly independent then it is unlikely that one would observe large values of `\(\chi^2\)` <!--D--> -- - In this case we reject the null and conclude the variables are dependent.<!--D--> -- - However, we can also think of the `\(\chi^2\)` stat as a measure of dependence where:<!--D--> -- - Small values indicate low dependence - Large values indicate high dependence --- # Inertia - Correspondence analysis is based on a similar idea.<!--D--> -- - However the counts in each cell `\(O_i\)` and `\(E_i\)` are replaced with probabilities `\(o_{ij}=\frac{O_{ij}}{n}\)` and `\(e_{ij}=E_{ij}/n\)`.<!--D--> -- - Each count is dividided by `\(n\)` which is the total of all cell counts (i.e. `\(r\times c\)`).<!--D--> -- - Instead of the `\(\chi^2\)` we get inertia defined as `$$\mbox{Inertia}=\frac{\chi^2}{n}$$` --- # Correspondence Analysis - Correspondence analysis is about explaining as much inertia as possible with a small number of dimensions.<!--D--> -- - Instead of the original rows and columns in the cross tab, a small number of linear combinations of these rows and columns are formed.<!--D--> -- - A good approximation to the original cross tab could be be reconstructed from these linear combinations. --- # Geometric Interpretation - Each column category can be plotted in `\(r\)`-dimensions.<!--D--> -- - Each row category can be plotted in `\(c\)`-dimensions.<!--D--> -- - Correspondence Analysis rotates both of these to provide the most interesting 'optimal' 2D visualisation<!--D--> -- - Here 'optimal' refers to maximising inertia. --- # Back to the output ```r summary(caout,row=FALSE,column=FALSE) ``` ``` ## ## Principal inertias (eigenvalues): ## ## dim value % cum% scree plot ## 1 0.193095 52.5 52.5 ************* ## 2 0.077731 21.1 73.6 ***** ## 3 0.043854 11.9 85.6 *** ## 4 0.032804 8.9 94.5 ** ## 5 0.012257 3.3 97.8 * ## 6 0.005687 1.5 99.4 ## 7 0.002363 0.6 100.0 ## -------- ----- ## Total: 0.367791 100.0 ``` --- # How to intepret this - Eigenvalues previously told us:<!--D--> -- - The variance explained by each principal component in PCA.<!--D--> -- - Give some indication of the Goodness of fit for MDS.<!--D--> -- - In CA the eigenvalues tell us the proportion of inertia explained by the solution.<!--D--> -- - A 2D solution is usually used for visualisation.<!--D--> -- - In the breakfast example the visualisation explains 73.6% of the inertia. --- class:inverse, middle, center # Matrix decompositions --- # Matrix decompositions - Wherever dimension reduction is used there is usually a matrix decomposition hidden somewhere. -- - In this case, the matrix that is decomposed is related to the cross tab. -- - In particular consider the values `$$m_{ij}=\frac{o_{ij}-e_{ij}}{\sqrt{e_{ij}}}$$` --- # What about CA? - Now consider a matrix `\({\mathbf M}\)` with `\(m_{ij}\)` in the `\(i^{th}\)` row and `\(j^{th}\)` column.<!--D--> -- - Let the SVD of this matrix be `$${\mathbf M}={\mathbf U}{\mathbf D}{\mathbf V}'$$`<!--D--> -- - We will consider<!--D--> -- + Post-multiplying by `\({\mathbf V}\)` + Pre-multiplying by `\({\mathbf U}'\)` --- # Post-multiplying by `\({\mathbf V}\)` - This gives `\({\mathbf U}{\mathbf D}{\mathbf V}'{\mathbf V}={\mathbf U}{\mathbf D}\)`<!--D--> -- - We get a *factor score* for each row category that is a linear combination of all column categories.<!--D--> -- - These are a bit like principal components for the row categories. --- # Pre-multiplying by `\({\mathbf U}'\)` - This gives `\({\mathbf U}'{\mathbf U}{\mathbf D}{\mathbf V}'={\mathbf D}{\mathbf V}'\)`<!--D--> -- - We get a *factor score* for each column category that is a linear combination of all row categories. -- - These are a bit like principal components for the column categories. --- # On the same plot - All the information can be summarised in a single plot using the biplot<!--D--> -- - For CA the symmetric normalisation is often used.<!--D--> -- - This means we plot the first two columns of `\({\mathbf U}{\mathbf D}^{1/2}\)` and `\({\mathbf V}{\mathbf D}^{1/2}\)`<!--D--> -- - This way we do not prioritise a more accurate representation neither for rows nor for columns. --- class: inverse, middle center # Application --- # Example: Hotel Reviews - For an interesting example related to marketing consider hotel reviews.<!--D--> -- - Many websites provide user reviews.<!--D--> -- - The words in each review can be scraped from the web<!--D--> -- - In the following example eight hotels in Melbourne were considered<!--D--> -- - Four that were highly rated: Crown Towers, Adelphi, Larwill and QT<!--D--> -- - Four that were not highly rated: Mercure, FlagstaffCity, Citiclub, Hotel Sophia --- # Example: Hotel Reviews - For each hotel, 100 reviews were scraped.<!--D--> -- - So called *stop words* ('the', 'a', 'is') were removed as were the names of the hotels.<!--D--> -- - The 20 most frequent words used for each hotel.<!--D--> -- - Combining these lists for 8 hotels led to 63 words (some words appear on multiple top 20 lists) --- # Example: Hotel Reviews - Jaccard similarity could be used to do MDS<!--D--> -- - However there are two interesting things that will not be captured by such an analysis<!--D--> -- + The frequency with which words appear is important.<!--D--> -- + The association between the hotels and words. --- # Example: Hotel Reviews - On Moodle you will find a cross tab featuring the frequency with which each word appeared on each review <div style="border: 1px solid #ddd; padding: 0px; overflow-y: scroll; height:350px; "><table class="table table-striped table-hover table-condensed" style="margin-left: auto; margin-right: auto;"> <thead> <tr> <th style="text-align:left;position: sticky; top:0; background-color: #FFFFFF;"> </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> Adelphi </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> Citiclub </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> CrownTowers </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> FlagstaffCity </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> HotelSophia </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> Larwill </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> Mercure </th> <th style="text-align:right;position: sticky; top:0; background-color: #FFFFFF;"> QT </th> </tr> </thead> <tbody> <tr> <td style="text-align:left;"> amazing </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 33 </td> </tr> <tr> <td style="text-align:left;"> bar </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 48 </td> </tr> <tr> <td style="text-align:left;"> bathroom </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 6 </td> </tr> <tr> <td style="text-align:left;"> bed </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 19 </td> </tr> <tr> <td style="text-align:left;"> booked </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 1 </td> </tr> <tr> <td style="text-align:left;"> breakfast </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 9 </td> </tr> <tr> <td style="text-align:left;"> city </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 12 </td> </tr> <tr> <td style="text-align:left;"> clean </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 22 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 37 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 7 </td> </tr> <tr> <td style="text-align:left;"> close </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 12 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 7 </td> </tr> <tr> <td style="text-align:left;"> club </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 64 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> comfortable </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 20 </td> <td style="text-align:right;"> 27 </td> </tr> <tr> <td style="text-align:left;"> cross </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> crown </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 89 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 2 </td> </tr> <tr> <td style="text-align:left;"> crystal </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> dear </td> <td style="text-align:right;"> 97 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 47 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 41 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> enjoyed </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 51 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 20 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 46 </td> </tr> <tr> <td style="text-align:left;"> experience </td> <td style="text-align:right;"> 36 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 39 </td> </tr> <tr> <td style="text-align:left;"> fantastic </td> <td style="text-align:right;"> 46 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 21 </td> </tr> <tr> <td style="text-align:left;"> feedback </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 66 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 39 </td> </tr> <tr> <td style="text-align:left;"> forward </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 51 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 4 </td> </tr> <tr> <td style="text-align:left;"> free </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 2 </td> </tr> <tr> <td style="text-align:left;"> friendly </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 30 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 35 </td> </tr> <tr> <td style="text-align:left;"> good </td> <td style="text-align:right;"> 12 </td> <td style="text-align:right;"> 67 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 29 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 27 </td> <td style="text-align:right;"> 14 </td> </tr> <tr> <td style="text-align:left;"> great </td> <td style="text-align:right;"> 71 </td> <td style="text-align:right;"> 76 </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 52 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 95 </td> </tr> <tr> <td style="text-align:left;"> hear </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 44 </td> </tr> <tr> <td style="text-align:left;"> hello </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 62 </td> </tr> <tr> <td style="text-align:left;"> helpful </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 12 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 16 </td> </tr> <tr> <td style="text-align:left;"> hi </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 29 </td> </tr> <tr> <td style="text-align:left;"> hotel </td> <td style="text-align:right;"> 74 </td> <td style="text-align:right;"> 172 </td> <td style="text-align:right;"> 49 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 61 </td> <td style="text-align:right;"> 51 </td> <td style="text-align:right;"> 65 </td> <td style="text-align:right;"> 67 </td> </tr> <tr> <td style="text-align:left;"> leave </td> <td style="text-align:right;"> 41 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 14 </td> </tr> <tr> <td style="text-align:left;"> location </td> <td style="text-align:right;"> 51 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 35 </td> </tr> <tr> <td style="text-align:left;"> lovely </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 30 </td> </tr> <tr> <td style="text-align:left;"> melbourne </td> <td style="text-align:right;"> 36 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 49 </td> <td style="text-align:right;"> 22 </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 19 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 77 </td> </tr> <tr> <td style="text-align:left;"> much </td> <td style="text-align:right;"> 45 </td> <td style="text-align:right;"> 61 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 40 </td> </tr> <tr> <td style="text-align:left;"> my </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 40 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 28 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 27 </td> </tr> <tr> <td style="text-align:left;"> night </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 53 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 13 </td> </tr> <tr> <td style="text-align:left;"> nights </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 27 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 4 </td> </tr> <tr> <td style="text-align:left;"> noise </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 64 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 1 </td> </tr> <tr> <td style="text-align:left;"> north </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> old </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 3 </td> </tr> <tr> <td style="text-align:left;"> park </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> </tr> <tr> <td style="text-align:left;"> parking </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 2 </td> </tr> <tr> <td style="text-align:left;"> place </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 17 </td> <td style="text-align:right;"> 12 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 8 </td> <td style="text-align:right;"> 11 </td> </tr> <tr> <td style="text-align:left;"> positive </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 55 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 6 </td> </tr> <tr> <td style="text-align:left;"> really </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 43 </td> </tr> <tr> <td style="text-align:left;"> receive </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 55 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> reception </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 6 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 27 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 3 </td> </tr> <tr> <td style="text-align:left;"> review </td> <td style="text-align:right;"> 49 </td> <td style="text-align:right;"> 98 </td> <td style="text-align:right;"> 39 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 37 </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 32 </td> </tr> <tr> <td style="text-align:left;"> room </td> <td style="text-align:right;"> 34 </td> <td style="text-align:right;"> 61 </td> <td style="text-align:right;"> 70 </td> <td style="text-align:right;"> 59 </td> <td style="text-align:right;"> 81 </td> <td style="text-align:right;"> 64 </td> <td style="text-align:right;"> 61 </td> <td style="text-align:right;"> 51 </td> </tr> <tr> <td style="text-align:left;"> rooms </td> <td style="text-align:right;"> 30 </td> <td style="text-align:right;"> 53 </td> <td style="text-align:right;"> 23 </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 34 </td> <td style="text-align:right;"> 33 </td> <td style="text-align:right;"> 35 </td> </tr> <tr> <td style="text-align:left;"> service </td> <td style="text-align:right;"> 41 </td> <td style="text-align:right;"> 57 </td> <td style="text-align:right;"> 30 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 10 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 40 </td> </tr> <tr> <td style="text-align:left;"> southern </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 18 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> staff </td> <td style="text-align:right;"> 72 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 35 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 25 </td> <td style="text-align:right;"> 44 </td> <td style="text-align:right;"> 41 </td> <td style="text-align:right;"> 76 </td> </tr> <tr> <td style="text-align:left;"> station </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> </tr> <tr> <td style="text-align:left;"> stay </td> <td style="text-align:right;"> 86 </td> <td style="text-align:right;"> 100 </td> <td style="text-align:right;"> 71 </td> <td style="text-align:right;"> 24 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 60 </td> <td style="text-align:right;"> 58 </td> <td style="text-align:right;"> 66 </td> </tr> <tr> <td style="text-align:left;"> stayed </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 32 </td> <td style="text-align:right;"> 33 </td> <td style="text-align:right;"> 31 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 26 </td> <td style="text-align:right;"> 29 </td> <td style="text-align:right;"> 23 </td> </tr> <tr> <td style="text-align:left;"> taking </td> <td style="text-align:right;"> 61 </td> <td style="text-align:right;"> 99 </td> <td style="text-align:right;"> 7 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 37 </td> </tr> <tr> <td style="text-align:left;"> team </td> <td style="text-align:right;"> 44 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 21 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 3 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 8 </td> </tr> <tr> <td style="text-align:left;"> thank </td> <td style="text-align:right;"> 89 </td> <td style="text-align:right;"> 127 </td> <td style="text-align:right;"> 51 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 38 </td> <td style="text-align:right;"> 27 </td> <td style="text-align:right;"> 44 </td> </tr> <tr> <td style="text-align:left;"> time </td> <td style="text-align:right;"> 86 </td> <td style="text-align:right;"> 99 </td> <td style="text-align:right;"> 15 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 9 </td> <td style="text-align:right;"> 36 </td> <td style="text-align:right;"> 29 </td> <td style="text-align:right;"> 51 </td> </tr> <tr> <td style="text-align:left;"> towers </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 69 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 0 </td> <td style="text-align:right;"> 1 </td> </tr> <tr> <td style="text-align:left;"> walk </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 13 </td> <td style="text-align:right;"> 14 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 5 </td> <td style="text-align:right;"> 4 </td> </tr> <tr> <td style="text-align:left;"> wonderful </td> <td style="text-align:right;"> 48 </td> <td style="text-align:right;"> 2 </td> <td style="text-align:right;"> 16 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 1 </td> <td style="text-align:right;"> 11 </td> <td style="text-align:right;"> 4 </td> <td style="text-align:right;"> 27 </td> </tr> </tbody> </table></div> --- # Example: Hotel Reviews The data can be loaded and correspondence analysis can be carried out using ```r load('hotels.RData') hoteltable%>%ca%>%plot ``` --- # Example: Hotel Reviews <img src="CA_files/figure-html/unnamed-chunk-4-1.png" style="display: block; margin: auto;" /> --- # Conclusions - Towards the bottom left of the plot are words like *wonderful*, *amazing* and *fantastic*. <!--D--> -- + The more highly rated hotels *Crown Towers*, *QT* and *Adelphi* are closer towards the bottom left<!--D--> -- - Towards the top of the plot the words *noise* and *club* appear together with the *Citiclub* hotel<!--D--> -- + This suggests that there may be complaints about noise from a night club. --- # Conclusions - Towards the right of the plot the word *old* appears as does *Hotel Sophia* and *Flagstaff*<!--D--> -- + These are lower rated hotels, the age of the hotels may be a problem.<!--D--> -- - Can you see anything else? --- # Example: Hotel Reviews ``` ## ## Principal inertias (eigenvalues): ## ## dim value % cum% scree plot ## 1 0.199004 27.1 27.1 ******* ## 2 0.184450 25.1 52.2 ****** ## 3 0.155095 21.1 73.3 ***** ## 4 0.075622 10.3 83.6 *** ## 5 0.058206 7.9 91.5 ** ## 6 0.038432 5.2 96.7 * ## 7 0.024115 3.3 100.0 * ## -------- ----- ## Total: 0.734923 100.0 ``` --- # Example: Hotel Reviews
<script>/* * Copyright (C) 2009 Apple Inc. All Rights Reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * Copyright (2016) Duncan Murdoch - fixed CanvasMatrix4.ortho, * cleaned up. */ /* CanvasMatrix4 class This class implements a 4x4 matrix. It has functions which duplicate the functionality of the OpenGL matrix stack and glut functions. IDL: [ Constructor(in CanvasMatrix4 matrix), // copy passed matrix into new CanvasMatrix4 Constructor(in sequence<float> array) // create new CanvasMatrix4 with 16 floats (row major) Constructor() // create new CanvasMatrix4 with identity matrix ] interface CanvasMatrix4 { attribute float m11; attribute float m12; attribute float m13; attribute float m14; attribute float m21; attribute float m22; attribute float m23; attribute float m24; attribute float m31; attribute float m32; attribute float m33; attribute float m34; attribute float m41; attribute float m42; attribute float m43; attribute float m44; void load(in CanvasMatrix4 matrix); // copy the values from the passed matrix void load(in sequence<float> array); // copy 16 floats into the matrix sequence<float> getAsArray(); // return the matrix as an array of 16 floats WebGLFloatArray getAsCanvasFloatArray(); // return the matrix as a WebGLFloatArray with 16 values void makeIdentity(); // replace the matrix with identity void transpose(); // replace the matrix with its transpose void invert(); // replace the matrix with its inverse void translate(in float x, in float y, in float z); // multiply the matrix by passed translation values on the right void scale(in float x, in float y, in float z); // multiply the matrix by passed scale values on the right void rotate(in float angle, // multiply the matrix by passed rotation values on the right in float x, in float y, in float z); // (angle is in degrees) void multRight(in CanvasMatrix matrix); // multiply the matrix by the passed matrix on the right void multLeft(in CanvasMatrix matrix); // multiply the matrix by the passed matrix on the left void ortho(in float left, in float right, // multiply the matrix by the passed ortho values on the right in float bottom, in float top, in float near, in float far); void frustum(in float left, in float right, // multiply the matrix by the passed frustum values on the right in float bottom, in float top, in float near, in float far); void perspective(in float fovy, in float aspect, // multiply the matrix by the passed perspective values on the right in float zNear, in float zFar); void lookat(in float eyex, in float eyey, in float eyez, // multiply the matrix by the passed lookat in float ctrx, in float ctry, in float ctrz, // values on the right in float upx, in float upy, in float upz); } */ CanvasMatrix4 = function(m) { if (typeof m == 'object') { if ("length" in m && m.length >= 16) { this.load(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15]); return; } else if (m instanceof CanvasMatrix4) { this.load(m); return; } } this.makeIdentity(); }; CanvasMatrix4.prototype.load = function() { if (arguments.length == 1 && typeof arguments[0] == 'object') { var matrix = arguments[0]; if ("length" in matrix && matrix.length == 16) { this.m11 = matrix[0]; this.m12 = matrix[1]; this.m13 = matrix[2]; this.m14 = matrix[3]; this.m21 = matrix[4]; this.m22 = matrix[5]; this.m23 = matrix[6]; this.m24 = matrix[7]; this.m31 = matrix[8]; this.m32 = matrix[9]; this.m33 = matrix[10]; this.m34 = matrix[11]; this.m41 = matrix[12]; this.m42 = matrix[13]; this.m43 = matrix[14]; this.m44 = matrix[15]; return; } if (arguments[0] instanceof CanvasMatrix4) { this.m11 = matrix.m11; this.m12 = matrix.m12; this.m13 = matrix.m13; this.m14 = matrix.m14; this.m21 = matrix.m21; this.m22 = matrix.m22; this.m23 = matrix.m23; this.m24 = matrix.m24; this.m31 = matrix.m31; this.m32 = matrix.m32; this.m33 = matrix.m33; this.m34 = matrix.m34; this.m41 = matrix.m41; this.m42 = matrix.m42; this.m43 = matrix.m43; this.m44 = matrix.m44; return; } } this.makeIdentity(); }; CanvasMatrix4.prototype.getAsArray = function() { return [ this.m11, this.m12, this.m13, this.m14, this.m21, this.m22, this.m23, this.m24, this.m31, this.m32, this.m33, this.m34, this.m41, this.m42, this.m43, this.m44 ]; }; CanvasMatrix4.prototype.getAsWebGLFloatArray = function() { return new WebGLFloatArray(this.getAsArray()); }; CanvasMatrix4.prototype.makeIdentity = function() { this.m11 = 1; this.m12 = 0; this.m13 = 0; this.m14 = 0; this.m21 = 0; this.m22 = 1; this.m23 = 0; this.m24 = 0; this.m31 = 0; this.m32 = 0; this.m33 = 1; this.m34 = 0; this.m41 = 0; this.m42 = 0; this.m43 = 0; this.m44 = 1; }; CanvasMatrix4.prototype.transpose = function() { var tmp = this.m12; this.m12 = this.m21; this.m21 = tmp; tmp = this.m13; this.m13 = this.m31; this.m31 = tmp; tmp = this.m14; this.m14 = this.m41; this.m41 = tmp; tmp = this.m23; this.m23 = this.m32; this.m32 = tmp; tmp = this.m24; this.m24 = this.m42; this.m42 = tmp; tmp = this.m34; this.m34 = this.m43; this.m43 = tmp; }; CanvasMatrix4.prototype.invert = function() { // Calculate the 4x4 determinant // If the determinant is zero, // then the inverse matrix is not unique. var det = this._determinant4x4(); if (Math.abs(det) < 1e-8) return null; this._makeAdjoint(); // Scale the adjoint matrix to get the inverse this.m11 /= det; this.m12 /= det; this.m13 /= det; this.m14 /= det; this.m21 /= det; this.m22 /= det; this.m23 /= det; this.m24 /= det; this.m31 /= det; this.m32 /= det; this.m33 /= det; this.m34 /= det; this.m41 /= det; this.m42 /= det; this.m43 /= det; this.m44 /= det; }; CanvasMatrix4.prototype.translate = function(x,y,z) { if (x === undefined) x = 0; if (y === undefined) y = 0; if (z === undefined) z = 0; var matrix = new CanvasMatrix4(); matrix.m41 = x; matrix.m42 = y; matrix.m43 = z; this.multRight(matrix); }; CanvasMatrix4.prototype.scale = function(x,y,z) { if (x === undefined) x = 1; if (z === undefined) { if (y === undefined) { y = x; z = x; } else z = 1; } else if (y === undefined) y = x; var matrix = new CanvasMatrix4(); matrix.m11 = x; matrix.m22 = y; matrix.m33 = z; this.multRight(matrix); }; CanvasMatrix4.prototype.rotate = function(angle,x,y,z) { // angles are in degrees. Switch to radians angle = angle / 180 * Math.PI; angle /= 2; var sinA = Math.sin(angle); var cosA = Math.cos(angle); var sinA2 = sinA * sinA; // normalize var length = Math.sqrt(x * x + y * y + z * z); if (length === 0) { // bad vector, just use something reasonable x = 0; y = 0; z = 1; } else if (length != 1) { x /= length; y /= length; z /= length; } var mat = new CanvasMatrix4(); // optimize case where axis is along major axis if (x == 1 && y === 0 && z === 0) { mat.m11 = 1; mat.m12 = 0; mat.m13 = 0; mat.m21 = 0; mat.m22 = 1 - 2 * sinA2; mat.m23 = 2 * sinA * cosA; mat.m31 = 0; mat.m32 = -2 * sinA * cosA; mat.m33 = 1 - 2 * sinA2; mat.m14 = mat.m24 = mat.m34 = 0; mat.m41 = mat.m42 = mat.m43 = 0; mat.m44 = 1; } else if (x === 0 && y == 1 && z === 0) { mat.m11 = 1 - 2 * sinA2; mat.m12 = 0; mat.m13 = -2 * sinA * cosA; mat.m21 = 0; mat.m22 = 1; mat.m23 = 0; mat.m31 = 2 * sinA * cosA; mat.m32 = 0; mat.m33 = 1 - 2 * sinA2; mat.m14 = mat.m24 = mat.m34 = 0; mat.m41 = mat.m42 = mat.m43 = 0; mat.m44 = 1; } else if (x === 0 && y === 0 && z == 1) { mat.m11 = 1 - 2 * sinA2; mat.m12 = 2 * sinA * cosA; mat.m13 = 0; mat.m21 = -2 * sinA * cosA; mat.m22 = 1 - 2 * sinA2; mat.m23 = 0; mat.m31 = 0; mat.m32 = 0; mat.m33 = 1; mat.m14 = mat.m24 = mat.m34 = 0; mat.m41 = mat.m42 = mat.m43 = 0; mat.m44 = 1; } else { var x2 = x*x; var y2 = y*y; var z2 = z*z; mat.m11 = 1 - 2 * (y2 + z2) * sinA2; mat.m12 = 2 * (x * y * sinA2 + z * sinA * cosA); mat.m13 = 2 * (x * z * sinA2 - y * sinA * cosA); mat.m21 = 2 * (y * x * sinA2 - z * sinA * cosA); mat.m22 = 1 - 2 * (z2 + x2) * sinA2; mat.m23 = 2 * (y * z * sinA2 + x * sinA * cosA); mat.m31 = 2 * (z * x * sinA2 + y * sinA * cosA); mat.m32 = 2 * (z * y * sinA2 - x * sinA * cosA); mat.m33 = 1 - 2 * (x2 + y2) * sinA2; mat.m14 = mat.m24 = mat.m34 = 0; mat.m41 = mat.m42 = mat.m43 = 0; mat.m44 = 1; } this.multRight(mat); }; CanvasMatrix4.prototype.multRight = function(mat) { var m11 = (this.m11 * mat.m11 + this.m12 * mat.m21 + this.m13 * mat.m31 + this.m14 * mat.m41); var m12 = (this.m11 * mat.m12 + this.m12 * mat.m22 + this.m13 * mat.m32 + this.m14 * mat.m42); var m13 = (this.m11 * mat.m13 + this.m12 * mat.m23 + this.m13 * mat.m33 + this.m14 * mat.m43); var m14 = (this.m11 * mat.m14 + this.m12 * mat.m24 + this.m13 * mat.m34 + this.m14 * mat.m44); var m21 = (this.m21 * mat.m11 + this.m22 * mat.m21 + this.m23 * mat.m31 + this.m24 * mat.m41); var m22 = (this.m21 * mat.m12 + this.m22 * mat.m22 + this.m23 * mat.m32 + this.m24 * mat.m42); var m23 = (this.m21 * mat.m13 + this.m22 * mat.m23 + this.m23 * mat.m33 + this.m24 * mat.m43); var m24 = (this.m21 * mat.m14 + this.m22 * mat.m24 + this.m23 * mat.m34 + this.m24 * mat.m44); var m31 = (this.m31 * mat.m11 + this.m32 * mat.m21 + this.m33 * mat.m31 + this.m34 * mat.m41); var m32 = (this.m31 * mat.m12 + this.m32 * mat.m22 + this.m33 * mat.m32 + this.m34 * mat.m42); var m33 = (this.m31 * mat.m13 + this.m32 * mat.m23 + this.m33 * mat.m33 + this.m34 * mat.m43); var m34 = (this.m31 * mat.m14 + this.m32 * mat.m24 + this.m33 * mat.m34 + this.m34 * mat.m44); var m41 = (this.m41 * mat.m11 + this.m42 * mat.m21 + this.m43 * mat.m31 + this.m44 * mat.m41); var m42 = (this.m41 * mat.m12 + this.m42 * mat.m22 + this.m43 * mat.m32 + this.m44 * mat.m42); var m43 = (this.m41 * mat.m13 + this.m42 * mat.m23 + this.m43 * mat.m33 + this.m44 * mat.m43); var m44 = (this.m41 * mat.m14 + this.m42 * mat.m24 + this.m43 * mat.m34 + this.m44 * mat.m44); this.m11 = m11; this.m12 = m12; this.m13 = m13; this.m14 = m14; this.m21 = m21; this.m22 = m22; this.m23 = m23; this.m24 = m24; this.m31 = m31; this.m32 = m32; this.m33 = m33; this.m34 = m34; this.m41 = m41; this.m42 = m42; this.m43 = m43; this.m44 = m44; }; CanvasMatrix4.prototype.multLeft = function(mat) { var m11 = (mat.m11 * this.m11 + mat.m12 * this.m21 + mat.m13 * this.m31 + mat.m14 * this.m41); var m12 = (mat.m11 * this.m12 + mat.m12 * this.m22 + mat.m13 * this.m32 + mat.m14 * this.m42); var m13 = (mat.m11 * this.m13 + mat.m12 * this.m23 + mat.m13 * this.m33 + mat.m14 * this.m43); var m14 = (mat.m11 * this.m14 + mat.m12 * this.m24 + mat.m13 * this.m34 + mat.m14 * this.m44); var m21 = (mat.m21 * this.m11 + mat.m22 * this.m21 + mat.m23 * this.m31 + mat.m24 * this.m41); var m22 = (mat.m21 * this.m12 + mat.m22 * this.m22 + mat.m23 * this.m32 + mat.m24 * this.m42); var m23 = (mat.m21 * this.m13 + mat.m22 * this.m23 + mat.m23 * this.m33 + mat.m24 * this.m43); var m24 = (mat.m21 * this.m14 + mat.m22 * this.m24 + mat.m23 * this.m34 + mat.m24 * this.m44); var m31 = (mat.m31 * this.m11 + mat.m32 * this.m21 + mat.m33 * this.m31 + mat.m34 * this.m41); var m32 = (mat.m31 * this.m12 + mat.m32 * this.m22 + mat.m33 * this.m32 + mat.m34 * this.m42); var m33 = (mat.m31 * this.m13 + mat.m32 * this.m23 + mat.m33 * this.m33 + mat.m34 * this.m43); var m34 = (mat.m31 * this.m14 + mat.m32 * this.m24 + mat.m33 * this.m34 + mat.m34 * this.m44); var m41 = (mat.m41 * this.m11 + mat.m42 * this.m21 + mat.m43 * this.m31 + mat.m44 * this.m41); var m42 = (mat.m41 * this.m12 + mat.m42 * this.m22 + mat.m43 * this.m32 + mat.m44 * this.m42); var m43 = (mat.m41 * this.m13 + mat.m42 * this.m23 + mat.m43 * this.m33 + mat.m44 * this.m43); var m44 = (mat.m41 * this.m14 + mat.m42 * this.m24 + mat.m43 * this.m34 + mat.m44 * this.m44); this.m11 = m11; this.m12 = m12; this.m13 = m13; this.m14 = m14; this.m21 = m21; this.m22 = m22; this.m23 = m23; this.m24 = m24; this.m31 = m31; this.m32 = m32; this.m33 = m33; this.m34 = m34; this.m41 = m41; this.m42 = m42; this.m43 = m43; this.m44 = m44; }; CanvasMatrix4.prototype.ortho = function(left, right, bottom, top, near, far) { var tx = (left + right) / (left - right); var ty = (top + bottom) / (bottom - top); var tz = (far + near) / (near - far); var matrix = new CanvasMatrix4(); matrix.m11 = 2 / (right - left); matrix.m12 = 0; matrix.m13 = 0; matrix.m14 = 0; matrix.m21 = 0; matrix.m22 = 2 / (top - bottom); matrix.m23 = 0; matrix.m24 = 0; matrix.m31 = 0; matrix.m32 = 0; matrix.m33 = -2 / (far - near); matrix.m34 = 0; matrix.m41 = tx; matrix.m42 = ty; matrix.m43 = tz; matrix.m44 = 1; this.multRight(matrix); }; CanvasMatrix4.prototype.frustum = function(left, right, bottom, top, near, far) { var matrix = new CanvasMatrix4(); var A = (right + left) / (right - left); var B = (top + bottom) / (top - bottom); var C = -(far + near) / (far - near); var D = -(2 * far * near) / (far - near); matrix.m11 = (2 * near) / (right - left); matrix.m12 = 0; matrix.m13 = 0; matrix.m14 = 0; matrix.m21 = 0; matrix.m22 = 2 * near / (top - bottom); matrix.m23 = 0; matrix.m24 = 0; matrix.m31 = A; matrix.m32 = B; matrix.m33 = C; matrix.m34 = -1; matrix.m41 = 0; matrix.m42 = 0; matrix.m43 = D; matrix.m44 = 0; this.multRight(matrix); }; CanvasMatrix4.prototype.perspective = function(fovy, aspect, zNear, zFar) { var top = Math.tan(fovy * Math.PI / 360) * zNear; var bottom = -top; var left = aspect * bottom; var right = aspect * top; this.frustum(left, right, bottom, top, zNear, zFar); }; CanvasMatrix4.prototype.lookat = function(eyex, eyey, eyez, centerx, centery, centerz, upx, upy, upz) { var matrix = new CanvasMatrix4(); // Make rotation matrix // Z vector var zx = eyex - centerx; var zy = eyey - centery; var zz = eyez - centerz; var mag = Math.sqrt(zx * zx + zy * zy + zz * zz); if (mag) { zx /= mag; zy /= mag; zz /= mag; } // Y vector var yx = upx; var yy = upy; var yz = upz; // X vector = Y cross Z xx = yy * zz - yz * zy; xy = -yx * zz + yz * zx; xz = yx * zy - yy * zx; // Recompute Y = Z cross X yx = zy * xz - zz * xy; yy = -zx * xz + zz * xx; yx = zx * xy - zy * xx; // cross product gives area of parallelogram, which is < 1.0 for // non-perpendicular unit-length vectors; so normalize x, y here mag = Math.sqrt(xx * xx + xy * xy + xz * xz); if (mag) { xx /= mag; xy /= mag; xz /= mag; } mag = Math.sqrt(yx * yx + yy * yy + yz * yz); if (mag) { yx /= mag; yy /= mag; yz /= mag; } matrix.m11 = xx; matrix.m12 = xy; matrix.m13 = xz; matrix.m14 = 0; matrix.m21 = yx; matrix.m22 = yy; matrix.m23 = yz; matrix.m24 = 0; matrix.m31 = zx; matrix.m32 = zy; matrix.m33 = zz; matrix.m34 = 0; matrix.m41 = 0; matrix.m42 = 0; matrix.m43 = 0; matrix.m44 = 1; matrix.translate(-eyex, -eyey, -eyez); this.multRight(matrix); }; // Support functions CanvasMatrix4.prototype._determinant2x2 = function(a, b, c, d) { return a * d - b * c; }; CanvasMatrix4.prototype._determinant3x3 = function(a1, a2, a3, b1, b2, b3, c1, c2, c3) { return a1 * this._determinant2x2(b2, b3, c2, c3) - b1 * this._determinant2x2(a2, a3, c2, c3) + c1 * this._determinant2x2(a2, a3, b2, b3); }; CanvasMatrix4.prototype._determinant4x4 = function() { var a1 = this.m11; var b1 = this.m12; var c1 = this.m13; var d1 = this.m14; var a2 = this.m21; var b2 = this.m22; var c2 = this.m23; var d2 = this.m24; var a3 = this.m31; var b3 = this.m32; var c3 = this.m33; var d3 = this.m34; var a4 = this.m41; var b4 = this.m42; var c4 = this.m43; var d4 = this.m44; return a1 * this._determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4) - b1 * this._determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4) + c1 * this._determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4) - d1 * this._determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4); }; CanvasMatrix4.prototype._makeAdjoint = function() { var a1 = this.m11; var b1 = this.m12; var c1 = this.m13; var d1 = this.m14; var a2 = this.m21; var b2 = this.m22; var c2 = this.m23; var d2 = this.m24; var a3 = this.m31; var b3 = this.m32; var c3 = this.m33; var d3 = this.m34; var a4 = this.m41; var b4 = this.m42; var c4 = this.m43; var d4 = this.m44; // Row column labeling reversed since we transpose rows & columns this.m11 = this._determinant3x3(b2, b3, b4, c2, c3, c4, d2, d3, d4); this.m21 = - this._determinant3x3(a2, a3, a4, c2, c3, c4, d2, d3, d4); this.m31 = this._determinant3x3(a2, a3, a4, b2, b3, b4, d2, d3, d4); this.m41 = - this._determinant3x3(a2, a3, a4, b2, b3, b4, c2, c3, c4); this.m12 = - this._determinant3x3(b1, b3, b4, c1, c3, c4, d1, d3, d4); this.m22 = this._determinant3x3(a1, a3, a4, c1, c3, c4, d1, d3, d4); this.m32 = - this._determinant3x3(a1, a3, a4, b1, b3, b4, d1, d3, d4); this.m42 = this._determinant3x3(a1, a3, a4, b1, b3, b4, c1, c3, c4); this.m13 = this._determinant3x3(b1, b2, b4, c1, c2, c4, d1, d2, d4); this.m23 = - this._determinant3x3(a1, a2, a4, c1, c2, c4, d1, d2, d4); this.m33 = this._determinant3x3(a1, a2, a4, b1, b2, b4, d1, d2, d4); this.m43 = - this._determinant3x3(a1, a2, a4, b1, b2, b4, c1, c2, c4); this.m14 = - this._determinant3x3(b1, b2, b3, c1, c2, c3, d1, d2, d3); this.m24 = this._determinant3x3(a1, a2, a3, c1, c2, c3, d1, d2, d3); this.m34 = - this._determinant3x3(a1, a2, a3, b1, b2, b3, d1, d2, d3); this.m44 = this._determinant3x3(a1, a2, a3, b1, b2, b3, c1, c2, c3); };</script> <script>// To generate the help pages for this library, use // jsdoc --destination ../../../doc/rglwidgetClass --template ~/node_modules/jsdoc-baseline rglClass.src.js // To validate, use // setwd(".../inst/htmlwidgets/lib/rglClass") // hints <- js::jshint(readLines("rglClass.src.js")) // hints[, c("line", "reason")] /** * The class of an rgl widget * @class */ rglwidgetClass = function() { this.canvas = null; this.userMatrix = new CanvasMatrix4(); this.types = []; this.prMatrix = new CanvasMatrix4(); this.mvMatrix = new CanvasMatrix4(); this.vp = null; this.prmvMatrix = null; this.origs = null; this.gl = null; this.scene = null; this.select = {state: "inactive", subscene: null, region: {p1: {x:0, y:0}, p2: {x:0, y:0}}}; this.drawing = false; }; /** * Multiply matrix by vector * @returns {number[]} * @param M {number[][]} Left operand * @param v {number[]} Right operand */ rglwidgetClass.prototype.multMV = function(M, v) { return [ M.m11 * v[0] + M.m12 * v[1] + M.m13 * v[2] + M.m14 * v[3], M.m21 * v[0] + M.m22 * v[1] + M.m23 * v[2] + M.m24 * v[3], M.m31 * v[0] + M.m32 * v[1] + M.m33 * v[2] + M.m34 * v[3], M.m41 * v[0] + M.m42 * v[1] + M.m43 * v[2] + M.m44 * v[3] ]; }; /** * Multiply row vector by Matrix * @returns {number[]} * @param v {number[]} left operand * @param M {number[][]} right operand */ rglwidgetClass.prototype.multVM = function(v, M) { return [ M.m11 * v[0] + M.m21 * v[1] + M.m31 * v[2] + M.m41 * v[3], M.m12 * v[0] + M.m22 * v[1] + M.m32 * v[2] + M.m42 * v[3], M.m13 * v[0] + M.m23 * v[1] + M.m33 * v[2] + M.m43 * v[3], M.m14 * v[0] + M.m24 * v[1] + M.m34 * v[2] + M.m44 * v[3] ]; }; /** * Euclidean length of a vector * @returns {number} * @param v {number[]} */ rglwidgetClass.prototype.vlen = function(v) { return Math.sqrt(this.dotprod(v, v)); }; /** * Dot product of two vectors * @instance rglwidgetClass * @returns {number} * @param a {number[]} * @param b {number[]} */ rglwidgetClass.prototype.dotprod = function(a, b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]; }; /** * Cross product of two vectors * @returns {number[]} * @param a {number[]} * @param b {number[]} */ rglwidgetClass.prototype.xprod = function(a, b) { return [a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]]; }; /** * Bind vectors or matrices by columns * @returns {number[][]} * @param a {number[]|number[][]} * @param b {number[]|number[][]} */ rglwidgetClass.prototype.cbind = function(a, b) { if (b.length < a.length) b = this.repeatToLen(b, a.length); else if (a.length < b.length) a = this.repeatToLen(a, b.length); return a.map(function(currentValue, index, array) { return currentValue.concat(b[index]); }); }; /** * Swap elements * @returns {any[]} * @param a {any[]} * @param i {number} Element to swap * @param j {number} Other element to swap */ rglwidgetClass.prototype.swap = function(a, i, j) { var temp = a[i]; a[i] = a[j]; a[j] = temp; }; /** * Flatten a matrix into a vector * @returns {any[]} * @param a {any[][]} */ rglwidgetClass.prototype.flatten = function(arr, result) { var value; if (typeof result === "undefined") result = []; for (var i = 0, length = arr.length; i < length; i++) { value = arr[i]; if (Array.isArray(value)) { this.flatten(value, result); } else { result.push(value); } } return result; }; /** * set element of 1d or 2d array as if it was flattened. * Column major, zero based! * @returns {any[]|any[][]} * @param {any[]|any[][]} a - array * @param {number} i - element * @param {any} value */ rglwidgetClass.prototype.setElement = function(a, i, value) { if (Array.isArray(a[0])) { var dim = a.length, col = Math.floor(i/dim), row = i % dim; a[row][col] = value; } else { a[i] = value; } }; /** * Transpose an array * @returns {any[][]} * @param {any[][]} a */ rglwidgetClass.prototype.transpose = function(a) { var newArray = [], n = a.length, m = a[0].length, i; for(i = 0; i < m; i++){ newArray.push([]); } for(i = 0; i < n; i++){ for(var j = 0; j < m; j++){ newArray[j].push(a[i][j]); } } return newArray; }; /** * Calculate sum of squares of a numeric vector * @returns {number} * @param {number[]} x */ rglwidgetClass.prototype.sumsq = function(x) { var result = 0, i; for (i=0; i < x.length; i++) result += x[i]*x[i]; return result; }; /** * Convert a matrix to a CanvasMatrix4 * @returns {CanvasMatrix4} * @param {number[][]|number[]} mat */ rglwidgetClass.prototype.toCanvasMatrix4 = function(mat) { if (mat instanceof CanvasMatrix4) return mat; var result = new CanvasMatrix4(); mat = this.flatten(this.transpose(mat)); result.load(mat); return result; }; /** * Convert an R-style numeric colour string to an rgb vector * @returns {number[]} * @param {string} s */ rglwidgetClass.prototype.stringToRgb = function(s) { s = s.replace("#", ""); var bigint = parseInt(s, 16); return [((bigint >> 16) & 255)/255, ((bigint >> 8) & 255)/255, (bigint & 255)/255]; }; /** * Take a component-by-component product of two 3 vectors * @returns {number[]} * @param {number[]} x * @param {number[]} y */ rglwidgetClass.prototype.componentProduct = function(x, y) { if (typeof y === "undefined") { this.alertOnce("Bad arg to componentProduct"); } var result = new Float32Array(3), i; for (i = 0; i<3; i++) result[i] = x[i]*y[i]; return result; }; /** * Get next higher power of two * @returns { number } * @param { number } value - input value */ rglwidgetClass.prototype.getPowerOfTwo = function(value) { var pow = 1; while(pow<value) { pow *= 2; } return pow; }; /** * Unique entries * @returns { any[] } * @param { any[] } arr - An array */ rglwidgetClass.prototype.unique = function(arr) { arr = [].concat(arr); return arr.filter(function(value, index, self) { return self.indexOf(value) === index; }); }; /** * Shallow compare of arrays * @returns { boolean } * @param { any[] } a - An array * @param { any[] } b - Another array */ rglwidgetClass.prototype.equalArrays = function(a, b) { return a === b || (a && b && a.length === b.length && a.every(function(v, i) {return v === b[i];})); }; /** * Repeat an array to a desired length * @returns {any[]} * @param {any | any[]} arr The input array * @param {number} len The desired output length */ rglwidgetClass.prototype.repeatToLen = function(arr, len) { arr = [].concat(arr); while (arr.length < len/2) arr = arr.concat(arr); return arr.concat(arr.slice(0, len - arr.length)); }; /** * Give a single alert message, not to be repeated. * @param {string} msg The message to give. */ rglwidgetClass.prototype.alertOnce = function(msg) { if (typeof this.alerted !== "undefined") return; this.alerted = true; alert(msg); }; rglwidgetClass.prototype.f_is_lit = 1; rglwidgetClass.prototype.f_is_smooth = 2; rglwidgetClass.prototype.f_has_texture = 4; rglwidgetClass.prototype.f_depth_sort = 8; rglwidgetClass.prototype.f_fixed_quads = 16; rglwidgetClass.prototype.f_is_transparent = 32; rglwidgetClass.prototype.f_is_lines = 64; rglwidgetClass.prototype.f_sprites_3d = 128; rglwidgetClass.prototype.f_sprite_3d = 256; rglwidgetClass.prototype.f_is_subscene = 512; rglwidgetClass.prototype.f_is_clipplanes = 1024; rglwidgetClass.prototype.f_fixed_size = 2048; rglwidgetClass.prototype.f_is_points = 4096; rglwidgetClass.prototype.f_is_twosided = 8192; rglwidgetClass.prototype.f_fat_lines = 16384; rglwidgetClass.prototype.f_is_brush = 32768; /** * Which list does a particular id come from? * @returns { string } * @param {number} id The id to look up. */ rglwidgetClass.prototype.whichList = function(id) { var obj = this.getObj(id), flags = obj.flags; if (obj.type === "light") return "lights"; if (flags & this.f_is_subscene) return "subscenes"; if (flags & this.f_is_clipplanes) return "clipplanes"; if (flags & this.f_is_transparent) return "transparent"; return "opaque"; }; /** * Get an object by id number. * @returns { Object } * @param {number} id */ rglwidgetClass.prototype.getObj = function(id) { if (typeof id !== "number") { this.alertOnce("getObj id is "+typeof id); } return this.scene.objects[id]; }; /** * Get ids of a particular type from a subscene or the whole scene * @returns { number[] } * @param {string} type What type of object? * @param {number} subscene Which subscene? If not given, find in the whole scene */ rglwidgetClass.prototype.getIdsByType = function(type, subscene) { var result = [], i, self = this; if (typeof subscene === "undefined") { Object.keys(this.scene.objects).forEach( function(key) { key = parseInt(key, 10); if (self.getObj(key).type === type) result.push(key); }); } else { ids = this.getObj(subscene).objects; for (i=0; i < ids.length; i++) { if (this.getObj(ids[i]).type === type) { result.push(ids[i]); } } } return result; }; /** * Get a particular material property for an id * @returns { any } * @param {number} id Which object? * @param {string} property Which material property? */ rglwidgetClass.prototype.getMaterial = function(id, property) { var obj = this.getObj(id), mat; if (typeof obj.material === "undefined") console.error("material undefined"); mat = obj.material[property]; if (typeof mat === "undefined") mat = this.scene.material[property]; return mat; }; /** * Is a particular id in a subscene? * @returns { boolean } * @param {number} id Which id? * @param {number} subscene Which subscene id? */ rglwidgetClass.prototype.inSubscene = function(id, subscene) { return this.getObj(subscene).objects.indexOf(id) > -1; }; /** * Add an id to a subscene. * @param {number} id Which id? * @param {number} subscene Which subscene id? */ rglwidgetClass.prototype.addToSubscene = function(id, subscene) { var thelist, thesub = this.getObj(subscene), ids = [id], obj = this.getObj(id), i; if (typeof obj != "undefined" && typeof (obj.newIds) !== "undefined") { ids = ids.concat(obj.newIds); } thesub.objects = [].concat(thesub.objects); for (i = 0; i < ids.length; i++) { id = ids[i]; if (thesub.objects.indexOf(id) == -1) { thelist = this.whichList(id); thesub.objects.push(id); thesub[thelist].push(id); } } }; /** * Delete an id from a subscene * @param { number } id - the id to add * @param { number } subscene - the id of the subscene */ rglwidgetClass.prototype.delFromSubscene = function(id, subscene) { var thelist, thesub = this.getObj(subscene), obj = this.getObj(id), ids = [id], i; if (typeof obj !== "undefined" && typeof (obj.newIds) !== "undefined") ids = ids.concat(obj.newIds); thesub.objects = [].concat(thesub.objects); // It might be a scalar for (j=0; j<ids.length;j++) { id = ids[j]; i = thesub.objects.indexOf(id); if (i > -1) { thesub.objects.splice(i, 1); thelist = this.whichList(id); i = thesub[thelist].indexOf(id); thesub[thelist].splice(i, 1); } } }; /** * Set the ids in a subscene * @param { number[] } ids - the ids to set * @param { number } subsceneid - the id of the subscene */ rglwidgetClass.prototype.setSubsceneEntries = function(ids, subsceneid) { var sub = this.getObj(subsceneid); sub.objects = ids; this.initSubscene(subsceneid); }; /** * Get the ids in a subscene * @returns {number[]} * @param { number } subscene - the id of the subscene */ rglwidgetClass.prototype.getSubsceneEntries = function(subscene) { return this.getObj(subscene).objects; }; /** * Get the ids of the subscenes within a subscene * @returns { number[] } * @param { number } subscene - the id of the subscene */ rglwidgetClass.prototype.getChildSubscenes = function(subscene) { return this.getObj(subscene).subscenes; }; /** * Start drawing * @returns { boolean } Previous state */ rglwidgetClass.prototype.startDrawing = function() { var value = this.drawing; this.drawing = true; return value; }; /** * Stop drawing and check for context loss * @param { boolean } saved - Previous state */ rglwidgetClass.prototype.stopDrawing = function(saved) { this.drawing = saved; if (!saved && this.gl && this.gl.isContextLost()) this.restartCanvas(); }; /** * Generate the vertex shader for an object * @returns {string} * @param { number } id - Id of object */ rglwidgetClass.prototype.getVertexShader = function(id) { var obj = this.getObj(id), userShader = obj.userVertexShader, flags = obj.flags, type = obj.type, is_lit = flags & this.f_is_lit, has_texture = flags & this.f_has_texture, fixed_quads = flags & this.f_fixed_quads, sprites_3d = flags & this.f_sprites_3d, sprite_3d = flags & this.f_sprite_3d, nclipplanes = this.countClipplanes(), fixed_size = flags & this.f_fixed_size, is_points = flags & this.f_is_points, is_twosided = flags & this.f_is_twosided, fat_lines = flags & this.f_fat_lines, is_brush = flags & this.f_is_brush, result; if (type === "clipplanes" || sprites_3d) return; if (typeof userShader !== "undefined") return userShader; result = " /* ****** "+type+" object "+id+" vertex shader ****** */\n"+ " attribute vec3 aPos;\n"+ " attribute vec4 aCol;\n"+ " uniform mat4 mvMatrix;\n"+ " uniform mat4 prMatrix;\n"+ " varying vec4 vCol;\n"+ " varying vec4 vPosition;\n"; if ((is_lit && !fixed_quads && !is_brush) || sprite_3d) result = result + " attribute vec3 aNorm;\n"+ " uniform mat4 normMatrix;\n"+ " varying vec3 vNormal;\n"; if (has_texture || type === "text") result = result + " attribute vec2 aTexcoord;\n"+ " varying vec2 vTexcoord;\n"; if (fixed_size) result = result + " uniform vec2 textScale;\n"; if (fixed_quads) result = result + " attribute vec2 aOfs;\n"; else if (sprite_3d) result = result + " uniform vec3 uOrig;\n"+ " uniform float uSize;\n"+ " uniform mat4 usermat;\n"; if (is_twosided) result = result + " attribute vec3 aPos1;\n"+ " attribute vec3 aPos2;\n"+ " varying float normz;\n"; if (fat_lines) { result = result + " attribute vec3 aNext;\n"+ " attribute vec2 aPoint;\n"+ " varying vec2 vPoint;\n"+ " varying float vLength;\n"+ " uniform float uAspect;\n"+ " uniform float uLwd;\n"; } result = result + " void main(void) {\n"; if ((nclipplanes || (!fixed_quads && !sprite_3d)) && !is_brush) result = result + " vPosition = mvMatrix * vec4(aPos, 1.);\n"; if (!fixed_quads && !sprite_3d && !is_brush) result = result + " gl_Position = prMatrix * vPosition;\n"; if (is_points) { var size = this.getMaterial(id, "size"); result = result + " gl_PointSize = "+size.toFixed(1)+";\n"; } result = result + " vCol = aCol;\n"; if (is_lit && !fixed_quads && !sprite_3d && !is_brush) result = result + " vNormal = normalize((normMatrix * vec4(aNorm, 1.)).xyz);\n"; if (has_texture || type == "text") result = result + " vTexcoord = aTexcoord;\n"; if (fixed_size) result = result + " vec4 pos = prMatrix * mvMatrix * vec4(aPos, 1.);\n"+ " pos = pos/pos.w;\n"+ " gl_Position = pos + vec4(aOfs*textScale, 0.,0.);\n"; if (type == "sprites" && !fixed_size) result = result + " vec4 pos = mvMatrix * vec4(aPos, 1.);\n"+ " pos = pos/pos.w + vec4(aOfs, 0., 0.);\n"+ " gl_Position = prMatrix*pos;\n"; if (sprite_3d) result = result + " vNormal = normalize((normMatrix * vec4(aNorm, 1.)).xyz);\n"+ " vec4 pos = mvMatrix * vec4(uOrig, 1.);\n"+ " vPosition = pos/pos.w + vec4(uSize*(vec4(aPos, 1.)*usermat).xyz,0.);\n"+ " gl_Position = prMatrix * vPosition;\n"; if (is_twosided) result = result + " vec4 pos1 = prMatrix*(mvMatrix*vec4(aPos1, 1.));\n"+ " pos1 = pos1/pos1.w - gl_Position/gl_Position.w;\n"+ " vec4 pos2 = prMatrix*(mvMatrix*vec4(aPos2, 1.));\n"+ " pos2 = pos2/pos2.w - gl_Position/gl_Position.w;\n"+ " normz = pos1.x*pos2.y - pos1.y*pos2.x;\n"; if (fat_lines) /* This code was inspired by Matt Deslauriers' code in https://mattdesl.svbtle.com/drawing-lines-is-hard */ result = result + " vec2 aspectVec = vec2(uAspect, 1.0);\n"+ " mat4 projViewModel = prMatrix * mvMatrix;\n"+ " vec4 currentProjected = projViewModel * vec4(aPos, 1.0);\n"+ " currentProjected = currentProjected/currentProjected.w;\n"+ " vec4 nextProjected = projViewModel * vec4(aNext, 1.0);\n"+ " vec2 currentScreen = currentProjected.xy * aspectVec;\n"+ " vec2 nextScreen = (nextProjected.xy / nextProjected.w) * aspectVec;\n"+ " float len = uLwd;\n"+ " vec2 dir = vec2(1.0, 0.0);\n"+ " vPoint = aPoint;\n"+ " vLength = length(nextScreen - currentScreen)/2.0;\n"+ " vLength = vLength/(vLength + len);\n"+ " if (vLength > 0.0) {\n"+ " dir = normalize(nextScreen - currentScreen);\n"+ " }\n"+ " vec2 normal = vec2(-dir.y, dir.x);\n"+ " dir.x /= uAspect;\n"+ " normal.x /= uAspect;\n"+ " vec4 offset = vec4(len*(normal*aPoint.x*aPoint.y - dir), 0.0, 0.0);\n"+ " gl_Position = currentProjected + offset;\n"; if (is_brush) result = result + " gl_Position = vec4(aPos, 1.);\n"; result = result + " }\n"; // console.log(result); return result; }; /** * Generate the fragment shader for an object * @returns {string} * @param { number } id - Id of object */ rglwidgetClass.prototype.getFragmentShader = function(id) { var obj = this.getObj(id), userShader = obj.userFragmentShader, flags = obj.flags, type = obj.type, is_lit = flags & this.f_is_lit, has_texture = flags & this.f_has_texture, fixed_quads = flags & this.f_fixed_quads, sprites_3d = flags & this.f_sprites_3d, is_twosided = (flags & this.f_is_twosided) > 0, fat_lines = flags & this.f_fat_lines, is_transparent = flags & this.f_is_transparent, nclipplanes = this.countClipplanes(), i, texture_format, nlights, result; if (type === "clipplanes" || sprites_3d) return; if (typeof userShader !== "undefined") return userShader; if (has_texture) texture_format = this.getMaterial(id, "textype"); result = "/* ****** "+type+" object "+id+" fragment shader ****** */\n"+ "#ifdef GL_ES\n"+ "#ifdef GL_FRAGMENT_PRECISION_HIGH\n"+ " precision highp float;\n"+ "#else\n"+ " precision mediump float;\n"+ "#endif\n"+ "#endif\n"+ " varying vec4 vCol; // carries alpha\n"+ " varying vec4 vPosition;\n"; if (has_texture || type === "text") result = result + " varying vec2 vTexcoord;\n"+ " uniform sampler2D uSampler;\n"; if (is_lit && !fixed_quads) result = result + " varying vec3 vNormal;\n"; for (i = 0; i < nclipplanes; i++) result = result + " uniform vec4 vClipplane"+i+";\n"; if (is_lit) { nlights = this.countLights(); if (nlights) result = result + " uniform mat4 mvMatrix;\n"; else is_lit = false; } if (is_lit) { result = result + " uniform vec3 emission;\n"+ " uniform float shininess;\n"; for (i=0; i < nlights; i++) { result = result + " uniform vec3 ambient" + i + ";\n"+ " uniform vec3 specular" + i +"; // light*material\n"+ " uniform vec3 diffuse" + i + ";\n"+ " uniform vec3 lightDir" + i + ";\n"+ " uniform bool viewpoint" + i + ";\n"+ " uniform bool finite" + i + ";\n"; } } if (is_twosided) result = result + " uniform bool front;\n"+ " varying float normz;\n"; if (fat_lines) result = result + " varying vec2 vPoint;\n"+ " varying float vLength;\n"; result = result + " void main(void) {\n"; if (fat_lines) { result = result + " vec2 point = vPoint;\n"+ " bool neg = point.y < 0.0;\n"+ " point.y = neg ? "+ " (point.y + vLength)/(1.0 - vLength) :\n"+ " -(point.y - vLength)/(1.0 - vLength);\n"; if (is_transparent && type == "linestrip") result = result+" if (neg && length(point) <= 1.0) discard;\n"; result = result + " point.y = min(point.y, 0.0);\n"+ " if (length(point) > 1.0) discard;\n"; } for (i=0; i < nclipplanes;i++) result = result + " if (dot(vPosition, vClipplane"+i+") < 0.0) discard;\n"; if (fixed_quads) { result = result + " vec3 n = vec3(0., 0., 1.);\n"; } else if (is_lit) { result = result + " vec3 n = normalize(vNormal);\n"; } if (is_twosided) { result = result + " if ((normz <= 0.) != front) discard;\n"; } if (is_lit) { result = result + " vec3 eye = normalize(-vPosition.xyz);\n"+ " vec3 lightdir;\n"+ " vec4 colDiff;\n"+ " vec3 halfVec;\n"+ " vec4 lighteffect = vec4(emission, 0.);\n"+ " vec3 col;\n"+ " float nDotL;\n"; if (!fixed_quads) { result = result + " n = -faceforward(n, n, eye);\n"; } for (i=0; i < nlights; i++) { result = result + " colDiff = vec4(vCol.rgb * diffuse" + i + ", vCol.a);\n"+ " lightdir = lightDir" + i + ";\n"+ " if (!viewpoint" + i +")\n"+ " lightdir = (mvMatrix * vec4(lightdir, 1.)).xyz;\n"+ " if (!finite" + i + ") {\n"+ " halfVec = normalize(lightdir + eye);\n"+ " } else {\n"+ " lightdir = normalize(lightdir - vPosition.xyz);\n"+ " halfVec = normalize(lightdir + eye);\n"+ " }\n"+ " col = ambient" + i + ";\n"+ " nDotL = dot(n, lightdir);\n"+ " col = col + max(nDotL, 0.) * colDiff.rgb;\n"+ " col = col + pow(max(dot(halfVec, n), 0.), shininess) * specular" + i + ";\n"+ " lighteffect = lighteffect + vec4(col, colDiff.a);\n"; } } else { result = result + " vec4 colDiff = vCol;\n"+ " vec4 lighteffect = colDiff;\n"; } if (type === "text") result = result + " vec4 textureColor = lighteffect*texture2D(uSampler, vTexcoord);\n"; if (has_texture) { result = result + { rgb: " vec4 textureColor = lighteffect*vec4(texture2D(uSampler, vTexcoord).rgb, 1.);\n", rgba: " vec4 textureColor = lighteffect*texture2D(uSampler, vTexcoord);\n", alpha: " vec4 textureColor = texture2D(uSampler, vTexcoord);\n"+ " float luminance = dot(vec3(1.,1.,1.), textureColor.rgb)/3.;\n"+ " textureColor = vec4(lighteffect.rgb, lighteffect.a*luminance);\n", luminance: " vec4 textureColor = vec4(lighteffect.rgb*dot(texture2D(uSampler, vTexcoord).rgb, vec3(1.,1.,1.))/3., lighteffect.a);\n", "luminance.alpha":" vec4 textureColor = texture2D(uSampler, vTexcoord);\n"+ " float luminance = dot(vec3(1.,1.,1.),textureColor.rgb)/3.;\n"+ " textureColor = vec4(lighteffect.rgb*luminance, lighteffect.a*textureColor.a);\n" }[texture_format]+ " gl_FragColor = textureColor;\n"; } else if (type === "text") { result = result + " if (textureColor.a < 0.1)\n"+ " discard;\n"+ " else\n"+ " gl_FragColor = textureColor;\n"; } else result = result + " gl_FragColor = lighteffect;\n"; //if (fat_lines) // result = result + " gl_FragColor = vec4(0.0, abs(point.x), abs(point.y), 1.0);" result = result + " }\n"; // console.log(result); return result; }; /** * Call gl functions to create and compile shader * @returns {Object} * @param { number } shaderType - gl code for shader type * @param { string } code - code for the shader */ rglwidgetClass.prototype.getShader = function(shaderType, code) { var gl = this.gl, shader; shader = gl.createShader(shaderType); gl.shaderSource(shader, code); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) alert(gl.getShaderInfoLog(shader)); return shader; }; /** * Handle a texture after its image has been loaded * @param { Object } texture - the gl texture object * @param { Object } textureCanvas - the canvas holding the image */ rglwidgetClass.prototype.handleLoadedTexture = function(texture, textureCanvas) { var gl = this.gl || this.initGL(); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureCanvas); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); gl.generateMipmap(gl.TEXTURE_2D); gl.bindTexture(gl.TEXTURE_2D, null); }; /** * Get maximum dimension of texture in current browser. * @returns {number} */ rglwidgetClass.prototype.getMaxTexSize = function() { var gl = this.gl || this.initGL(); return Math.min(4096, gl.getParameter(gl.MAX_TEXTURE_SIZE)); }; /** * Load an image to a texture * @param { string } uri - The image location * @param { Object } texture - the gl texture object */ rglwidgetClass.prototype.loadImageToTexture = function(uri, texture) { var canvas = this.textureCanvas, ctx = canvas.getContext("2d"), image = new Image(), self = this; image.onload = function() { var w = image.width, h = image.height, canvasX = self.getPowerOfTwo(w), canvasY = self.getPowerOfTwo(h), gl = self.gl || self.initGL(), maxTexSize = self.getMaxTexSize(); while (canvasX > 1 && canvasY > 1 && (canvasX > maxTexSize || canvasY > maxTexSize)) { canvasX /= 2; canvasY /= 2; } canvas.width = canvasX; canvas.height = canvasY; ctx.imageSmoothingEnabled = true; ctx.drawImage(image, 0, 0, canvasX, canvasY); self.handleLoadedTexture(texture, canvas); self.drawScene(); }; image.src = uri; }; /** * Draw text to the texture canvas * @returns { Object } object with text measurements * @param { string } text - the text * @param { number } cex - expansion * @param { string } family - font family * @param { number } font - font number */ rglwidgetClass.prototype.drawTextToCanvas = function(text, cex, family, font) { var canvasX, canvasY, textY, scaling = 20, textColour = "white", backgroundColour = "rgba(0,0,0,0)", canvas = this.textureCanvas, ctx = canvas.getContext("2d"), i, textHeight = 0, textHeights = [], width, widths = [], offsetx, offsety = 0, line, lines = [], offsetsx = [], offsetsy = [], lineoffsetsy = [], fontStrings = [], maxTexSize = this.getMaxTexSize(), getFontString = function(i) { textHeights[i] = scaling*cex[i]; var fontString = textHeights[i] + "px", family0 = family[i], font0 = font[i]; if (family0 === "sans") family0 = "sans-serif"; else if (family0 === "mono") family0 = "monospace"; fontString = fontString + " " + family0; if (font0 === 2 || font0 === 4) fontString = "bold " + fontString; if (font0 === 3 || font0 === 4) fontString = "italic " + fontString; return fontString; }; cex = this.repeatToLen(cex, text.length); family = this.repeatToLen(family, text.length); font = this.repeatToLen(font, text.length); canvasX = 1; line = -1; offsetx = maxTexSize; for (i = 0; i < text.length; i++) { ctx.font = fontStrings[i] = getFontString(i); width = widths[i] = ctx.measureText(text[i]).width; if (offsetx + width > maxTexSize) { line += 1; offsety = lineoffsetsy[line] = offsety + 2*textHeight; if (offsety > maxTexSize) console.error("Too many strings for texture."); textHeight = 0; offsetx = 0; } textHeight = Math.max(textHeight, textHeights[i]); offsetsx[i] = offsetx; offsetx += width; canvasX = Math.max(canvasX, offsetx); lines[i] = line; } offsety = lineoffsetsy[line] = offsety + 2*textHeight; for (i = 0; i < text.length; i++) { offsetsy[i] = lineoffsetsy[lines[i]]; } canvasX = this.getPowerOfTwo(canvasX); canvasY = this.getPowerOfTwo(offsety); canvas.width = canvasX; canvas.height = canvasY; ctx.fillStyle = backgroundColour; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.textBaseline = "alphabetic"; for(i = 0; i < text.length; i++) { ctx.font = fontStrings[i]; ctx.fillStyle = textColour; ctx.textAlign = "left"; ctx.fillText(text[i], offsetsx[i], offsetsy[i]); } return {canvasX:canvasX, canvasY:canvasY, widths:widths, textHeights:textHeights, offsetsx:offsetsx, offsetsy:offsetsy}; }; /** * Set the gl viewport and scissor test * @param { number } id - id of subscene */ rglwidgetClass.prototype.setViewport = function(id) { var gl = this.gl || this.initGL(), vp = this.getObj(id).par3d.viewport, x = vp.x*this.canvas.width, y = vp.y*this.canvas.height, width = vp.width*this.canvas.width, height = vp.height*this.canvas.height; this.vp = {x:x, y:y, width:width, height:height}; gl.viewport(x, y, width, height); gl.scissor(x, y, width, height); gl.enable(gl.SCISSOR_TEST); }; /** * Set the projection matrix for a subscene * @param { number } id - id of subscene */ rglwidgetClass.prototype.setprMatrix = function(id) { var subscene = this.getObj(id), embedding = subscene.embeddings.projection; if (embedding === "replace") this.prMatrix.makeIdentity(); else this.setprMatrix(subscene.parent); if (embedding === "inherit") return; // This is based on the Frustum::enclose code from geom.cpp var bbox = subscene.par3d.bbox, scale = subscene.par3d.scale, ranges = [(bbox[1]-bbox[0])*scale[0]/2, (bbox[3]-bbox[2])*scale[1]/2, (bbox[5]-bbox[4])*scale[2]/2], radius = Math.sqrt(this.sumsq(ranges))*1.1; // A bit bigger to handle labels if (radius <= 0) radius = 1; var observer = subscene.par3d.observer, distance = observer[2], FOV = subscene.par3d.FOV, ortho = FOV === 0, t = ortho ? 1 : Math.tan(FOV*Math.PI/360), near = distance - radius, far = distance + radius, hlen, aspect = this.vp.width/this.vp.height, z = subscene.par3d.zoom, userProjection = subscene.par3d.userProjection; if (far < 0.0) far = 1.0; if (near < far/100.0) near = far/100.0; hlen = t*near; if (ortho) { if (aspect > 1) this.prMatrix.ortho(-hlen*aspect*z, hlen*aspect*z, -hlen*z, hlen*z, near, far); else this.prMatrix.ortho(-hlen*z, hlen*z, -hlen*z/aspect, hlen*z/aspect, near, far); } else { if (aspect > 1) this.prMatrix.frustum(-hlen*aspect*z, hlen*aspect*z, -hlen*z, hlen*z, near, far); else this.prMatrix.frustum(-hlen*z, hlen*z, -hlen*z/aspect, hlen*z/aspect, near, far); } this.prMatrix.multRight(userProjection); }; /** * Set the model-view matrix for a subscene * @param { number } id - id of the subscene */ rglwidgetClass.prototype.setmvMatrix = function(id) { var observer = this.getObj(id).par3d.observer; this.mvMatrix.makeIdentity(); this.setmodelMatrix(id); this.mvMatrix.translate(-observer[0], -observer[1], -observer[2]); }; /** * Set the model matrix for a subscene * @param { number } id - id of the subscene */ rglwidgetClass.prototype.setmodelMatrix = function(id) { var subscene = this.getObj(id), embedding = subscene.embeddings.model; if (embedding !== "inherit") { var scale = subscene.par3d.scale, bbox = subscene.par3d.bbox, center = [(bbox[0]+bbox[1])/2, (bbox[2]+bbox[3])/2, (bbox[4]+bbox[5])/2]; this.mvMatrix.translate(-center[0], -center[1], -center[2]); this.mvMatrix.scale(scale[0], scale[1], scale[2]); this.mvMatrix.multRight( subscene.par3d.userMatrix ); } if (embedding !== "replace") this.setmodelMatrix(subscene.parent); }; /** * Set the normals matrix for a subscene * @param { number } subsceneid - id of the subscene */ rglwidgetClass.prototype.setnormMatrix = function(subsceneid) { var self = this, recurse = function(id) { var sub = self.getObj(id), embedding = sub.embeddings.model; if (embedding !== "inherit") { var scale = sub.par3d.scale; self.normMatrix.scale(1/scale[0], 1/scale[1], 1/scale[2]); self.normMatrix.multRight(sub.par3d.userMatrix); } if (embedding !== "replace") recurse(sub.parent); }; self.normMatrix.makeIdentity(); recurse(subsceneid); }; /** * Set the combined projection-model-view matrix */ rglwidgetClass.prototype.setprmvMatrix = function() { this.prmvMatrix = new CanvasMatrix4( this.mvMatrix ); this.prmvMatrix.multRight( this.prMatrix ); }; /** * Count clipping planes in a scene * @returns {number} */ rglwidgetClass.prototype.countClipplanes = function() { return this.countObjs("clipplanes"); }; /** * Count lights in a scene * @returns { number } */ rglwidgetClass.prototype.countLights = function() { return this.countObjs("light"); }; /** * Count objects of specific type in a scene * @returns { number } * @param { string } type - Type of object to count */ rglwidgetClass.prototype.countObjs = function(type) { var self = this, bound = 0; Object.keys(this.scene.objects).forEach( function(key) { if (self.getObj(parseInt(key, 10)).type === type) bound = bound + 1; }); return bound; }; /** * Initialize a subscene * @param { number } id - id of subscene. */ rglwidgetClass.prototype.initSubscene = function(id) { var sub = this.getObj(id), i, obj; if (sub.type !== "subscene") return; sub.par3d.userMatrix = this.toCanvasMatrix4(sub.par3d.userMatrix); sub.par3d.userProjection = this.toCanvasMatrix4(sub.par3d.userProjection); sub.par3d.userProjection.transpose(); sub.par3d.listeners = [].concat(sub.par3d.listeners); sub.backgroundId = undefined; sub.subscenes = []; sub.clipplanes = []; sub.transparent = []; sub.opaque = []; sub.lights = []; for (i=0; i < sub.objects.length; i++) { obj = this.getObj(sub.objects[i]); if (typeof obj === "undefined") { sub.objects.splice(i, 1); i--; } else if (obj.type === "background") sub.backgroundId = obj.id; else sub[this.whichList(obj.id)].push(obj.id); } }; /** * Copy object * @param { number } id - id of object to copy * @param { string } reuse - Document id of scene to reuse */ rglwidgetClass.prototype.copyObj = function(id, reuse) { var obj = this.getObj(id), prev = document.getElementById(reuse); if (prev !== null) { prev = prev.rglinstance; var prevobj = prev.getObj(id), fields = ["flags", "type", "colors", "vertices", "centers", "normals", "offsets", "texts", "cex", "family", "font", "adj", "material", "radii", "texcoords", "userMatrix", "ids", "dim", "par3d", "userMatrix", "viewpoint", "finite", "pos"], i; for (i = 0; i < fields.length; i++) { if (typeof prevobj[fields[i]] !== "undefined") obj[fields[i]] = prevobj[fields[i]]; } } else console.warn("copyObj failed"); }; /** * Update the triangles used to display a plane * @param { number } id - id of the plane * @param { Object } bbox - bounding box in which to display the plane */ rglwidgetClass.prototype.planeUpdateTriangles = function(id, bbox) { var perms = [[0,0,1], [1,2,2], [2,1,0]], x, xrow, elem, A, d, nhits, i, j, k, u, v, w, intersect, which, v0, v2, vx, reverse, face1 = [], face2 = [], normals = [], obj = this.getObj(id), nPlanes = obj.normals.length; obj.bbox = bbox; obj.vertices = []; obj.initialized = false; for (elem = 0; elem < nPlanes; elem++) { // Vertex Av = normal.getRecycled(elem); x = []; A = obj.normals[elem]; d = obj.offsets[elem][0]; nhits = 0; for (i=0; i<3; i++) for (j=0; j<2; j++) for (k=0; k<2; k++) { u = perms[0][i]; v = perms[1][i]; w = perms[2][i]; if (A[w] !== 0.0) { intersect = -(d + A[u]*bbox[j+2*u] + A[v]*bbox[k+2*v])/A[w]; if (bbox[2*w] < intersect && intersect < bbox[1+2*w]) { xrow = []; xrow[u] = bbox[j+2*u]; xrow[v] = bbox[k+2*v]; xrow[w] = intersect; x.push(xrow); face1[nhits] = j + 2*u; face2[nhits] = k + 2*v; nhits++; } } } if (nhits > 3) { /* Re-order the intersections so the triangles work */ for (i=0; i<nhits-2; i++) { which = 0; /* initialize to suppress warning */ for (j=i+1; j<nhits; j++) { if (face1[i] == face1[j] || face1[i] == face2[j] || face2[i] == face1[j] || face2[i] == face2[j] ) { which = j; break; } } if (which > i+1) { this.swap(x, i+1, which); this.swap(face1, i+1, which); this.swap(face2, i+1, which); } } } if (nhits >= 3) { /* Put in order so that the normal points out the FRONT of the faces */ v0 = [x[0][0] - x[1][0] , x[0][1] - x[1][1], x[0][2] - x[1][2]]; v2 = [x[2][0] - x[1][0] , x[2][1] - x[1][1], x[2][2] - x[1][2]]; /* cross-product */ vx = this.xprod(v0, v2); reverse = this.dotprod(vx, A) > 0; for (i=0; i<nhits-2; i++) { obj.vertices.push(x[0]); normals.push(A); for (j=1; j<3; j++) { obj.vertices.push(x[i + (reverse ? 3-j : j)]); normals.push(A); } } } } obj.pnormals = normals; }; rglwidgetClass.prototype.getAdj = function (pos, offset, text) { switch(pos) { case 1: return [0.5, 1 + offset]; case 2: return [1 + offset/text.length, 0.5]; case 3: return [0.5, -offset]; case 4: return [-offset/text.length, 0.5]; } } /** * Initialize object for display * @param { number } id - id of object to initialize */ rglwidgetClass.prototype.initObj = function(id) { var obj = this.getObj(id), flags = obj.flags, type = obj.type, is_lit = flags & this.f_is_lit, is_lines = flags & this.f_is_lines, fat_lines = flags & this.f_fat_lines, has_texture = flags & this.f_has_texture, fixed_quads = flags & this.f_fixed_quads, is_transparent = obj.is_transparent, depth_sort = flags & this.f_depth_sort, sprites_3d = flags & this.f_sprites_3d, sprite_3d = flags & this.f_sprite_3d, fixed_size = flags & this.f_fixed_size, is_twosided = (flags & this.f_is_twosided) > 0, is_brush = flags & this.f_is_brush, gl = this.gl || this.initGL(), polygon_offset, texinfo, drawtype, nclipplanes, f, nrows, oldrows, i,j,v,v1,v2, mat, uri, matobj, pass, passes, pmode, dim, nx, nz, attr; if (typeof id !== "number") { this.alertOnce("initObj id is "+typeof id); } obj.initialized = true; if (type === "bboxdeco" || type === "subscene") return; if (type === "light") { obj.ambient = new Float32Array(obj.colors[0].slice(0,3)); obj.diffuse = new Float32Array(obj.colors[1].slice(0,3)); obj.specular = new Float32Array(obj.colors[2].slice(0,3)); obj.lightDir = new Float32Array(obj.vertices[0]); return; } if (type === "clipplanes") { obj.vClipplane = this.flatten(this.cbind(obj.normals, obj.offsets)); return; } if (type === "background" && typeof obj.ids !== "undefined") { obj.quad = this.flatten([].concat(obj.ids)); return; } polygon_offset = this.getMaterial(id, "polygon_offset"); if (polygon_offset[0] != 0 || polygon_offset[1] != 0) obj.polygon_offset = polygon_offset; if (is_transparent) { depth_sort = ["triangles", "quads", "surface", "spheres", "sprites", "text"].indexOf(type) >= 0; } if (is_brush) this.initSelection(id); if (typeof obj.vertices === "undefined") obj.vertices = []; v = obj.vertices; obj.vertexCount = v.length; if (!obj.vertexCount) return; if (is_twosided) { if (typeof obj.userAttributes === "undefined") obj.userAttributes = {}; v1 = Array(v.length); v2 = Array(v.length); if (obj.type == "triangles" || obj.type == "quads") { if (obj.type == "triangles") nrow = 3; else nrow = 4; for (i=0; i<Math.floor(v.length/nrow); i++) for (j=0; j<nrow; j++) { v1[nrow*i + j] = v[nrow*i + ((j+1) % nrow)]; v2[nrow*i + j] = v[nrow*i + ((j+2) % nrow)]; } } else if (obj.type == "surface") { dim = obj.dim[0]; nx = dim[0]; nz = dim[1]; for (j=0; j<nx; j++) { for (i=0; i<nz; i++) { if (i+1 < nz && j+1 < nx) { v2[j + nx*i] = v[j + nx*(i+1)]; v1[j + nx*i] = v[j+1 + nx*(i+1)]; } else if (i+1 < nz) { v2[j + nx*i] = v[j-1 + nx*i]; v1[j + nx*i] = v[j + nx*(i+1)]; } else { v2[j + nx*i] = v[j + nx*(i-1)]; v1[j + nx*i] = v[j-1 + nx*(i-1)]; } } } } obj.userAttributes.aPos1 = v1; obj.userAttributes.aPos2 = v2; } if (!sprites_3d) { if (gl.isContextLost()) return; obj.prog = gl.createProgram(); gl.attachShader(obj.prog, this.getShader( gl.VERTEX_SHADER, this.getVertexShader(id) )); gl.attachShader(obj.prog, this.getShader( gl.FRAGMENT_SHADER, this.getFragmentShader(id) )); // Force aPos to location 0, aCol to location 1 gl.bindAttribLocation(obj.prog, 0, "aPos"); gl.bindAttribLocation(obj.prog, 1, "aCol"); gl.linkProgram(obj.prog); var linked = gl.getProgramParameter(obj.prog, gl.LINK_STATUS); if (!linked) { // An error occurred while linking var lastError = gl.getProgramInfoLog(obj.prog); console.warn("Error in program linking:" + lastError); gl.deleteProgram(obj.prog); return; } } if (type === "text") { texinfo = this.drawTextToCanvas(obj.texts, this.flatten(obj.cex), this.flatten(obj.family), this.flatten(obj.family)); } if (fixed_quads && !sprites_3d) { obj.ofsLoc = gl.getAttribLocation(obj.prog, "aOfs"); } if (sprite_3d) { obj.origLoc = gl.getUniformLocation(obj.prog, "uOrig"); obj.sizeLoc = gl.getUniformLocation(obj.prog, "uSize"); obj.usermatLoc = gl.getUniformLocation(obj.prog, "usermat"); } if (has_texture || type == "text") { if (!obj.texture) obj.texture = gl.createTexture(); obj.texLoc = gl.getAttribLocation(obj.prog, "aTexcoord"); obj.sampler = gl.getUniformLocation(obj.prog, "uSampler"); } if (has_texture) { mat = obj.material; if (typeof mat.uri !== "undefined") uri = mat.uri; else if (typeof mat.uriElementId === "undefined") { matobj = this.getObj(mat.uriId); if (typeof matobj !== "undefined") { uri = matobj.material.uri; } else { uri = ""; } } else uri = document.getElementById(mat.uriElementId).rglinstance.getObj(mat.uriId).material.uri; this.loadImageToTexture(uri, obj.texture); } if (type === "text") { this.handleLoadedTexture(obj.texture, this.textureCanvas); } var stride = 3, nc, cofs, nofs, radofs, oofs, tofs, vnew, fnew, nextofs = -1, pointofs = -1, alias, colors, key, selection, filter, adj, pos, offset; obj.alias = undefined; colors = obj.colors; j = this.scene.crosstalk.id.indexOf(id); if (j >= 0) { key = this.scene.crosstalk.key[j]; options = this.scene.crosstalk.options[j]; colors = colors.slice(0); for (i = 0; i < v.length; i++) colors[i] = obj.colors[i % obj.colors.length].slice(0); if ( (selection = this.scene.crosstalk.selection) && (selection.length || !options.selectedIgnoreNone) ) for (i = 0; i < v.length; i++) { if (!selection.includes(key[i])) { if (options.deselectedColor) colors[i] = options.deselectedColor.slice(0); colors[i][3] = colors[i][3]*options.deselectedFade; /* default: mostly transparent if not selected */ } else if (options.selectedColor) colors[i] = options.selectedColor.slice(0); } if ( (filter = this.scene.crosstalk.filter) ) for (i = 0; i < v.length; i++) if (!filter.includes(key[i])) { if (options.filteredColor) colors[i] = options.filteredColor.slice(0); colors[i][3] = colors[i][3]*options.filteredFade; /* default: completely hidden if filtered */ } } nc = obj.colorCount = colors.length; if (nc > 1) { cofs = stride; stride = stride + 4; v = this.cbind(v, colors); } else { cofs = -1; obj.onecolor = this.flatten(colors); } if (typeof obj.normals !== "undefined") { nofs = stride; stride = stride + 3; v = this.cbind(v, typeof obj.pnormals !== "undefined" ? obj.pnormals : obj.normals); } else nofs = -1; if (typeof obj.radii !== "undefined") { radofs = stride; stride = stride + 1; // FIXME: always concat the radii? if (obj.radii.length === v.length) { v = this.cbind(v, obj.radii); } else if (obj.radii.length === 1) { v = v.map(function(row, i, arr) { return row.concat(obj.radii[0]);}); } } else radofs = -1; // Add default indices f = Array(v.length); for (i = 0; i < v.length; i++) f[i] = i; obj.f = [f,f]; if (type == "sprites" && !sprites_3d) { tofs = stride; stride += 2; oofs = stride; stride += 2; vnew = new Array(4*v.length); fnew = new Array(4*v.length); alias = new Array(v.length); var rescale = fixed_size ? 72 : 1, size = obj.radii, s = rescale*size[0]/2; last = v.length; f = obj.f[0]; for (i=0; i < v.length; i++) { if (size.length > 1) s = rescale*size[i]/2; vnew[i] = v[i].concat([0,0,-s,-s]); fnew[4*i] = f[i]; vnew[last]= v[i].concat([1,0, s,-s]); fnew[4*i+1] = last++; vnew[last]= v[i].concat([1,1, s, s]); fnew[4*i+2] = last++; vnew[last]= v[i].concat([0,1,-s, s]); fnew[4*i+3] = last++; alias[i] = [last-3, last-2, last-1]; } v = vnew; obj.vertexCount = v.length; obj.f = [fnew, fnew]; } else if (type === "text") { tofs = stride; stride += 2; oofs = stride; stride += 2; vnew = new Array(4*v.length); f = obj.f[0]; fnew = new Array(4*f.length); alias = new Array(v.length); last = v.length; adj = this.flatten(obj.adj); if (typeof obj.pos !== "undefined") { pos = this.flatten(obj.pos); offset = adj[0]; } for (i=0; i < v.length; i++) { if (typeof pos !== "undefined") adj = this.getAdj(pos[i % pos.length], offset, obj.texts[i]); vnew[i] = v[i].concat([0,-0.5]).concat(adj); fnew[4*i] = f[i]; vnew[last] = v[i].concat([1,-0.5]).concat(adj); fnew[4*i+1] = last++; vnew[last] = v[i].concat([1, 1.5]).concat(adj); fnew[4*i+2] = last++; vnew[last] = v[i].concat([0, 1.5]).concat(adj); fnew[4*i+3] = last++; alias[i] = [last-3, last-2, last-1]; for (j=0; j < 4; j++) { v1 = vnew[fnew[4*i+j]]; v1[tofs+2] = 2*(v1[tofs]-v1[tofs+2])*texinfo.widths[i]; v1[tofs+3] = 2*(v1[tofs+1]-v1[tofs+3])*texinfo.textHeights[i]; v1[tofs] = (texinfo.offsetsx[i] + v1[tofs]*texinfo.widths[i])/texinfo.canvasX; v1[tofs+1] = 1.0-(texinfo.offsetsy[i] - v1[tofs+1]*texinfo.textHeights[i])/texinfo.canvasY; vnew[fnew[4*i+j]] = v1; } } v = vnew; obj.vertexCount = v.length; obj.f = [fnew, fnew]; } else if (typeof obj.texcoords !== "undefined") { tofs = stride; stride += 2; oofs = -1; v = this.cbind(v, obj.texcoords); } else { tofs = -1; oofs = -1; } obj.alias = alias; if (typeof obj.userAttributes !== "undefined") { obj.userAttribOffsets = {}; obj.userAttribLocations = {}; obj.userAttribSizes = {}; for (attr in obj.userAttributes) { obj.userAttribLocations[attr] = gl.getAttribLocation(obj.prog, attr); if (obj.userAttribLocations[attr] >= 0) { // Attribute may not have been used obj.userAttribOffsets[attr] = stride; v = this.cbind(v, obj.userAttributes[attr]); stride = v[0].length; obj.userAttribSizes[attr] = stride - obj.userAttribOffsets[attr]; } } } if (typeof obj.userUniforms !== "undefined") { obj.userUniformLocations = {}; for (attr in obj.userUniforms) obj.userUniformLocations[attr] = gl.getUniformLocation(obj.prog, attr); } if (sprites_3d) { obj.userMatrix = new CanvasMatrix4(obj.userMatrix); obj.objects = this.flatten([].concat(obj.ids)); is_lit = false; for (i=0; i < obj.objects.length; i++) this.initObj(obj.objects[i]); } if (is_lit && !fixed_quads) { obj.normLoc = gl.getAttribLocation(obj.prog, "aNorm"); } nclipplanes = this.countClipplanes(); if (nclipplanes && !sprites_3d) { obj.clipLoc = []; for (i=0; i < nclipplanes; i++) obj.clipLoc[i] = gl.getUniformLocation(obj.prog,"vClipplane" + i); } if (is_lit) { obj.emissionLoc = gl.getUniformLocation(obj.prog, "emission"); obj.emission = new Float32Array(this.stringToRgb(this.getMaterial(id, "emission"))); obj.shininessLoc = gl.getUniformLocation(obj.prog, "shininess"); obj.shininess = this.getMaterial(id, "shininess"); obj.nlights = this.countLights(); obj.ambientLoc = []; obj.ambient = new Float32Array(this.stringToRgb(this.getMaterial(id, "ambient"))); obj.specularLoc = []; obj.specular = new Float32Array(this.stringToRgb(this.getMaterial(id, "specular"))); obj.diffuseLoc = []; obj.lightDirLoc = []; obj.viewpointLoc = []; obj.finiteLoc = []; for (i=0; i < obj.nlights; i++) { obj.ambientLoc[i] = gl.getUniformLocation(obj.prog, "ambient" + i); obj.specularLoc[i] = gl.getUniformLocation(obj.prog, "specular" + i); obj.diffuseLoc[i] = gl.getUniformLocation(obj.prog, "diffuse" + i); obj.lightDirLoc[i] = gl.getUniformLocation(obj.prog, "lightDir" + i); obj.viewpointLoc[i] = gl.getUniformLocation(obj.prog, "viewpoint" + i); obj.finiteLoc[i] = gl.getUniformLocation(obj.prog, "finite" + i); } } obj.passes = is_twosided + 1; obj.pmode = new Array(obj.passes); for (pass = 0; pass < obj.passes; pass++) { if (type === "triangles" || type === "quads" || type === "surface") pmode = this.getMaterial(id, (pass === 0) ? "front" : "back"); else pmode = "filled"; obj.pmode[pass] = pmode; } obj.f.length = obj.passes; for (pass = 0; pass < obj.passes; pass++) { f = fnew = obj.f[pass]; pmode = obj.pmode[pass]; if (pmode === "culled") f = []; else if (pmode === "points") { // stay with default } else if ((type === "quads" || type === "text" || type === "sprites") && !sprites_3d) { nrows = Math.floor(obj.vertexCount/4); if (pmode === "filled") { fnew = Array(6*nrows); for (i=0; i < nrows; i++) { fnew[6*i] = f[4*i]; fnew[6*i+1] = f[4*i + 1]; fnew[6*i+2] = f[4*i + 2]; fnew[6*i+3] = f[4*i]; fnew[6*i+4] = f[4*i + 2]; fnew[6*i+5] = f[4*i + 3]; } } else { fnew = Array(8*nrows); for (i=0; i < nrows; i++) { fnew[8*i] = f[4*i]; fnew[8*i+1] = f[4*i + 1]; fnew[8*i+2] = f[4*i + 1]; fnew[8*i+3] = f[4*i + 2]; fnew[8*i+4] = f[4*i + 2]; fnew[8*i+5] = f[4*i + 3]; fnew[8*i+6] = f[4*i + 3]; fnew[8*i+7] = f[4*i]; } } } else if (type === "triangles") { nrows = Math.floor(obj.vertexCount/3); if (pmode === "filled") { fnew = Array(3*nrows); for (i=0; i < fnew.length; i++) { fnew[i] = f[i]; } } else if (pmode === "lines") { fnew = Array(6*nrows); for (i=0; i < nrows; i++) { fnew[6*i] = f[3*i]; fnew[6*i + 1] = f[3*i + 1]; fnew[6*i + 2] = f[3*i + 1]; fnew[6*i + 3] = f[3*i + 2]; fnew[6*i + 4] = f[3*i + 2]; fnew[6*i + 5] = f[3*i]; } } } else if (type === "spheres") { // default } else if (type === "surface") { dim = obj.dim[0]; nx = dim[0]; nz = dim[1]; if (pmode === "filled") { fnew = []; for (j=0; j<nx-1; j++) { for (i=0; i<nz-1; i++) { fnew.push(f[j + nx*i], f[j + nx*(i+1)], f[j + 1 + nx*(i+1)], f[j + nx*i], f[j + 1 + nx*(i+1)], f[j + 1 + nx*i]); } } } else if (pmode === "lines") { fnew = []; for (j=0; j<nx; j++) { for (i=0; i<nz; i++) { if (i+1 < nz) fnew.push(f[j + nx*i], f[j + nx*(i+1)]); if (j+1 < nx) fnew.push(f[j + nx*i], f[j+1 + nx*i]); } } } } obj.f[pass] = fnew; if (depth_sort) { drawtype = "DYNAMIC_DRAW"; } else { drawtype = "STATIC_DRAW"; } } if (fat_lines) { alias = undefined; obj.nextLoc = gl.getAttribLocation(obj.prog, "aNext"); obj.pointLoc = gl.getAttribLocation(obj.prog, "aPoint"); obj.aspectLoc = gl.getUniformLocation(obj.prog, "uAspect"); obj.lwdLoc = gl.getUniformLocation(obj.prog, "uLwd"); // Expand vertices to turn each segment into a pair of triangles for (pass = 0; pass < obj.passes; pass++) { f = obj.f[pass]; oldrows = f.length; if (obj.pmode[pass] === "lines") break; } if (type === "linestrip") nrows = 4*(oldrows - 1); else nrows = 2*oldrows; vnew = new Array(nrows); fnew = new Array(1.5*nrows); var fnext = new Array(nrows), fpt = new Array(nrows), pt, start, gap = type === "linestrip" ? 3 : 1; // We're going to turn each pair of vertices into 4 new ones, with the "next" and "pt" attributes // added. // We do this by copying the originals in the first pass, adding the new attributes, then in a // second pass add new vertices at the end. for (i = 0; i < v.length; i++) { vnew[i] = v[i].concat([0,0,0,0,0]); } nextofs = stride; pointofs = stride + 3; stride = stride + 5; // Now add the extras last = v.length - 1; ind = 0; alias = new Array(f.length); for (i = 0; i < f.length; i++) alias[i] = []; for (i = 0; i < f.length - 1; i++) { if (type !== "linestrip" && i % 2 == 1) continue; k = ++last; vnew[k] = vnew[f[i]].slice(); for (j=0; j<3; j++) vnew[k][nextofs + j] = vnew[f[i+1]][j]; vnew[k][pointofs] = -1; vnew[k][pointofs+1] = -1; fnew[ind] = k; last++; vnew[last] = vnew[k].slice(); vnew[last][pointofs] = 1; fnew[ind+1] = last; alias[f[i]].push(last-1, last); last++; k = last; vnew[k] = vnew[f[i+1]].slice(); for (j=0; j<3; j++) vnew[k][nextofs + j] = vnew[f[i]][j]; vnew[k][pointofs] = -1; vnew[k][pointofs+1] = 1; fnew[ind+2] = k; fnew[ind+3] = fnew[ind+1]; last++; vnew[last] = vnew[k].slice(); vnew[last][pointofs] = 1; fnew[ind+4] = last; fnew[ind+5] = fnew[ind+2]; ind += 6; alias[f[i+1]].push(last-1, last); } vnew.length = last+1; v = vnew; obj.vertexCount = v.length; if (typeof alias !== "undefined" && typeof obj.alias !== "undefined") { // Already have aliases from previous section? var oldalias = obj.alias, newalias = Array(obj.alias.length); for (i = 0; i < newalias.length; i++) { newalias[i] = oldalias[i].slice(); for (j = 0; j < oldalias[i].length; j++) Array.prototype.push.apply(newalias[i], alias[oldalias[j]]); // pushes each element } obj.alias = newalias; } else obj.alias = alias; for (pass = 0; pass < obj.passes; pass++) if (type === "lines" || type === "linestrip" || obj.pmode[pass] == "lines") { obj.f[pass] = fnew; } if (depth_sort) drawtype = "DYNAMIC_DRAW"; else drawtype = "STATIC_DRAW"; } for (pass = 0; pass < obj.passes; pass++) { if (obj.vertexCount > 65535) { if (this.index_uint) { obj.f[pass] = new Uint32Array(obj.f[pass]); obj.index_uint = true; } else this.alertOnce("Object has "+obj.vertexCount+" vertices, not supported in this browser."); } else { obj.f[pass] = new Uint16Array(obj.f[pass]); obj.index_uint = false; } } if (stride !== v[0].length) { this.alertOnce("problem in stride calculation"); } obj.vOffsets = {vofs:0, cofs:cofs, nofs:nofs, radofs:radofs, oofs:oofs, tofs:tofs, nextofs:nextofs, pointofs:pointofs, stride:stride}; obj.values = new Float32Array(this.flatten(v)); if (type !== "spheres" && !sprites_3d) { obj.buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, obj.buf); gl.bufferData(gl.ARRAY_BUFFER, obj.values, gl.STATIC_DRAW); // obj.ibuf = Array(obj.passes); obj.ibuf[0] = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.ibuf[0]); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, obj.f[0], gl[drawtype]); if (is_twosided) { obj.ibuf[1] = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.ibuf[1]); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, obj.f[1], gl[drawtype]); } } if (!sprites_3d) { obj.mvMatLoc = gl.getUniformLocation(obj.prog, "mvMatrix"); obj.prMatLoc = gl.getUniformLocation(obj.prog, "prMatrix"); } if (fixed_size) { obj.textScaleLoc = gl.getUniformLocation(obj.prog, "textScale"); } if (is_lit && !sprites_3d) { obj.normMatLoc = gl.getUniformLocation(obj.prog, "normMatrix"); } if (is_twosided) { obj.frontLoc = gl.getUniformLocation(obj.prog, "front"); } }; /** * Set gl depth test based on object's material * @param { number } id - object to use */ rglwidgetClass.prototype.setDepthTest = function(id) { var gl = this.gl || this.initGL(), tests = {never: gl.NEVER, less: gl.LESS, equal: gl.EQUAL, lequal:gl.LEQUAL, greater: gl.GREATER, notequal: gl.NOTEQUAL, gequal: gl.GEQUAL, always: gl.ALWAYS}, test = tests[this.getMaterial(id, "depth_test")]; gl.depthFunc(test); }; rglwidgetClass.prototype.mode4type = {points : "POINTS", linestrip : "LINE_STRIP", abclines : "LINES", lines : "LINES", sprites : "TRIANGLES", planes : "TRIANGLES", text : "TRIANGLES", quads : "TRIANGLES", surface : "TRIANGLES", triangles : "TRIANGLES"}; /** * Sort objects from back to front * @returns { number[] } * @param { Object } obj - object to sort */ rglwidgetClass.prototype.depthSort = function(obj) { var n = obj.centers.length, depths = new Float32Array(n), result = new Array(n), compare = function(i,j) { return depths[j] - depths[i]; }, z, w; for(i=0; i<n; i++) { z = this.prmvMatrix.m13*obj.centers[i][0] + this.prmvMatrix.m23*obj.centers[i][1] + this.prmvMatrix.m33*obj.centers[i][2] + this.prmvMatrix.m43; w = this.prmvMatrix.m14*obj.centers[i][0] + this.prmvMatrix.m24*obj.centers[i][1] + this.prmvMatrix.m34*obj.centers[i][2] + this.prmvMatrix.m44; depths[i] = z/w; result[i] = i; } result.sort(compare); return result; }; rglwidgetClass.prototype.disableArrays = function(obj, enabled) { var gl = this.gl || this.initGL(), objLocs = ["normLoc", "texLoc", "ofsLoc", "pointLoc", "nextLoc"], thisLocs = ["posLoc", "colLoc"], i, attr; for (i = 0; i < objLocs.length; i++) if (enabled[objLocs[i]]) gl.disableVertexAttribArray(obj[objLocs[i]]); for (i = 0; i < thisLocs.length; i++) if (enabled[thisLocs[i]]) gl.disableVertexAttribArray(this[objLocs[i]]); if (typeof obj.userAttributes !== "undefined") { for (attr in obj.userAttribSizes) { // Not all attributes may have been used gl.disableVertexAttribArray( obj.userAttribLocations[attr] ); } } } /** * Draw an object in a subscene * @param { number } id - object to draw * @param { number } subsceneid - id of subscene */ rglwidgetClass.prototype.drawObj = function(id, subsceneid) { var obj = this.getObj(id), subscene = this.getObj(subsceneid), flags = obj.flags, type = obj.type, is_lit = flags & this.f_is_lit, has_texture = flags & this.f_has_texture, fixed_quads = flags & this.f_fixed_quads, is_transparent = flags & this.f_is_transparent, depth_sort = flags & this.f_depth_sort, sprites_3d = flags & this.f_sprites_3d, sprite_3d = flags & this.f_sprite_3d, is_lines = flags & this.f_is_lines, fat_lines = flags & this.f_fat_lines, is_points = flags & this.f_is_points, fixed_size = flags & this.f_fixed_size, is_twosided = (flags & this.f_is_twosided) > 0, gl = this.gl || this.initGL(), mat, sphereMV, baseofs, ofs, sscale, i, count, light, pass, mode, pmode, attr, enabled = {}; if (typeof id !== "number") { this.alertOnce("drawObj id is "+typeof id); } if (type === "planes") { if (obj.bbox !== subscene.par3d.bbox || !obj.initialized) { this.planeUpdateTriangles(id, subscene.par3d.bbox); } } if (!obj.initialized) this.initObj(id); if (type === "clipplanes") { count = obj.offsets.length; var IMVClip = []; for (i=0; i < count; i++) { IMVClip[i] = this.multMV(this.invMatrix, obj.vClipplane.slice(4*i, 4*(i+1))); } obj.IMVClip = IMVClip; return; } if (type === "light" || type === "bboxdeco" || !obj.vertexCount) return; if (!is_transparent && obj.someHidden) { is_transparent = true; depth_sort = ["triangles", "quads", "surface", "spheres", "sprites", "text"].indexOf(type) >= 0; } this.setDepthTest(id); if (sprites_3d) { var norigs = obj.vertices.length, savenorm = new CanvasMatrix4(this.normMatrix); this.origs = obj.vertices; this.usermat = new Float32Array(obj.userMatrix.getAsArray()); this.radii = obj.radii; this.normMatrix = subscene.spriteNormmat; for (this.iOrig=0; this.iOrig < norigs; this.iOrig++) { for (i=0; i < obj.objects.length; i++) { this.drawObj(obj.objects[i], subsceneid); } } this.normMatrix = savenorm; return; } else { gl.useProgram(obj.prog); } if (typeof obj.polygon_offset !== "undefined") { gl.polygonOffset(obj.polygon_offset[0], obj.polygon_offset[1]); gl.enable(gl.POLYGON_OFFSET_FILL); } if (sprite_3d) { gl.uniform3fv(obj.origLoc, new Float32Array(this.origs[this.iOrig])); if (this.radii.length > 1) { gl.uniform1f(obj.sizeLoc, this.radii[this.iOrig][0]); } else { gl.uniform1f(obj.sizeLoc, this.radii[0][0]); } gl.uniformMatrix4fv(obj.usermatLoc, false, this.usermat); } if (type === "spheres") { gl.bindBuffer(gl.ARRAY_BUFFER, this.sphere.buf); } else { gl.bindBuffer(gl.ARRAY_BUFFER, obj.buf); } gl.uniformMatrix4fv( obj.prMatLoc, false, new Float32Array(this.prMatrix.getAsArray()) ); gl.uniformMatrix4fv( obj.mvMatLoc, false, new Float32Array(this.mvMatrix.getAsArray()) ); var clipcheck = 0, clipplaneids = subscene.clipplanes, clip, j; for (i=0; i < clipplaneids.length; i++) { clip = this.getObj(clipplaneids[i]); for (j=0; j < clip.offsets.length; j++) { gl.uniform4fv(obj.clipLoc[clipcheck + j], clip.IMVClip[j]); } clipcheck += clip.offsets.length; } if (typeof obj.clipLoc !== "undefined") for (i=clipcheck; i < obj.clipLoc.length; i++) gl.uniform4f(obj.clipLoc[i], 0,0,0,0); if (is_lit) { gl.uniformMatrix4fv( obj.normMatLoc, false, new Float32Array(this.normMatrix.getAsArray()) ); gl.uniform3fv( obj.emissionLoc, obj.emission); gl.uniform1f( obj.shininessLoc, obj.shininess); for (i=0; i < subscene.lights.length; i++) { light = this.getObj(subscene.lights[i]); if (!light.initialized) this.initObj(subscene.lights[i]); gl.uniform3fv( obj.ambientLoc[i], this.componentProduct(light.ambient, obj.ambient)); gl.uniform3fv( obj.specularLoc[i], this.componentProduct(light.specular, obj.specular)); gl.uniform3fv( obj.diffuseLoc[i], light.diffuse); gl.uniform3fv( obj.lightDirLoc[i], light.lightDir); gl.uniform1i( obj.viewpointLoc[i], light.viewpoint); gl.uniform1i( obj.finiteLoc[i], light.finite); } for (i=subscene.lights.length; i < obj.nlights; i++) { gl.uniform3f( obj.ambientLoc[i], 0,0,0); gl.uniform3f( obj.specularLoc[i], 0,0,0); gl.uniform3f( obj.diffuseLoc[i], 0,0,0); } } if (fixed_size) { gl.uniform2f( obj.textScaleLoc, 0.75/this.vp.width, 0.75/this.vp.height); } gl.enableVertexAttribArray( this.posLoc ); enabled.posLoc = true; var nc = obj.colorCount; count = obj.vertexCount; if (type === "spheres") { subscene = this.getObj(subsceneid); var scale = subscene.par3d.scale, scount = count, indices; gl.vertexAttribPointer(this.posLoc, 3, gl.FLOAT, false, 4*this.sphere.vOffsets.stride, 0); gl.enableVertexAttribArray(obj.normLoc ); enabled.normLoc = true; gl.vertexAttribPointer(obj.normLoc, 3, gl.FLOAT, false, 4*this.sphere.vOffsets.stride, 0); gl.disableVertexAttribArray( this.colLoc ); var sphereNorm = new CanvasMatrix4(); sphereNorm.scale(scale[0], scale[1], scale[2]); sphereNorm.multRight(this.normMatrix); gl.uniformMatrix4fv( obj.normMatLoc, false, new Float32Array(sphereNorm.getAsArray()) ); if (nc == 1) { gl.vertexAttrib4fv( this.colLoc, new Float32Array(obj.onecolor)); } if (has_texture) { gl.enableVertexAttribArray( obj.texLoc ); enabled.texLoc = true; gl.vertexAttribPointer(obj.texLoc, 2, gl.FLOAT, false, 4*this.sphere.vOffsets.stride, 4*this.sphere.vOffsets.tofs); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, obj.texture); gl.uniform1i( obj.sampler, 0); } if (depth_sort) indices = this.depthSort(obj); for (i = 0; i < scount; i++) { sphereMV = new CanvasMatrix4(); if (depth_sort) { baseofs = indices[i]*obj.vOffsets.stride; } else { baseofs = i*obj.vOffsets.stride; } ofs = baseofs + obj.vOffsets.radofs; sscale = obj.values[ofs]; sphereMV.scale(sscale/scale[0], sscale/scale[1], sscale/scale[2]); sphereMV.translate(obj.values[baseofs], obj.values[baseofs+1], obj.values[baseofs+2]); sphereMV.multRight(this.mvMatrix); gl.uniformMatrix4fv( obj.mvMatLoc, false, new Float32Array(sphereMV.getAsArray()) ); if (nc > 1) { ofs = baseofs + obj.vOffsets.cofs; gl.vertexAttrib4f( this.colLoc, obj.values[ofs], obj.values[ofs+1], obj.values[ofs+2], obj.values[ofs+3] ); } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.sphere.ibuf); gl.drawElements(gl.TRIANGLES, this.sphere.sphereCount, gl.UNSIGNED_SHORT, 0); } this.disableArrays(obj, enabled); if (typeof obj.polygon_offset !== "undefined") gl.disable(gl.POLYGON_OFFSET_FILL); return; } else { if (obj.colorCount === 1) { gl.disableVertexAttribArray( this.colLoc ); gl.vertexAttrib4fv( this.colLoc, new Float32Array(obj.onecolor)); } else { gl.enableVertexAttribArray( this.colLoc ); enabled.colLoc = true; gl.vertexAttribPointer(this.colLoc, 4, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.cofs); } } if (is_lit && obj.vOffsets.nofs > 0) { gl.enableVertexAttribArray( obj.normLoc ); enabled.normLoc = true; gl.vertexAttribPointer(obj.normLoc, 3, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.nofs); } if (has_texture || type === "text") { gl.enableVertexAttribArray( obj.texLoc ); enabled.texLoc = true; gl.vertexAttribPointer(obj.texLoc, 2, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.tofs); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, obj.texture); gl.uniform1i( obj.sampler, 0); } if (fixed_quads) { gl.enableVertexAttribArray( obj.ofsLoc ); enabled.ofsLoc = true; gl.vertexAttribPointer(obj.ofsLoc, 2, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.oofs); } if (typeof obj.userAttributes !== "undefined") { for (attr in obj.userAttribSizes) { // Not all attributes may have been used gl.enableVertexAttribArray( obj.userAttribLocations[attr] ); gl.vertexAttribPointer( obj.userAttribLocations[attr], obj.userAttribSizes[attr], gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.userAttribOffsets[attr]); } } if (typeof obj.userUniforms !== "undefined") { for (attr in obj.userUniformLocations) { var loc = obj.userUniformLocations[attr]; if (loc !== null) { var uniform = obj.userUniforms[attr]; if (typeof uniform.length === "undefined") gl.uniform1f(loc, uniform); else if (typeof uniform[0].length === "undefined") { uniform = new Float32Array(uniform); switch(uniform.length) { case 2: gl.uniform2fv(loc, uniform); break; case 3: gl.uniform3fv(loc, uniform); break; case 4: gl.uniform4fv(loc, uniform); break; default: console.warn("bad uniform length"); } } else if (uniform.length == 4 && uniform[0].length == 4) gl.uniformMatrix4fv(loc, false, new Float32Array(uniform.getAsArray())); else console.warn("unsupported uniform matrix"); } } } for (pass = 0; pass < obj.passes; pass++) { pmode = obj.pmode[pass]; if (pmode === "culled") continue; mode = fat_lines && (is_lines || pmode == "lines") ? "TRIANGLES" : this.mode4type[type]; if (depth_sort && pmode == "filled") {// Don't try depthsorting on wireframe or points var faces = this.depthSort(obj), nfaces = faces.length, frowsize = Math.floor(obj.f[pass].length/nfaces); if (type !== "spheres") { var f = obj.index_uint ? new Uint32Array(obj.f[pass].length) : new Uint16Array(obj.f[pass].length); for (i=0; i<nfaces; i++) { for (j=0; j<frowsize; j++) { f[frowsize*i + j] = obj.f[pass][frowsize*faces[i] + j]; } } gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.ibuf[pass]); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, f, gl.DYNAMIC_DRAW); } } if (is_twosided) gl.uniform1i(obj.frontLoc, pass !== 0); if (type !== "spheres") gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj.ibuf[pass]); if (type === "sprites" || type === "text" || type === "quads") { count = count * 6/4; } else if (type === "surface") { count = obj.f[pass].length; } count = obj.f[pass].length; if (!is_lines && pmode === "lines" && !fat_lines) { mode = "LINES"; } else if (pmode === "points") { mode = "POINTS"; } if ((is_lines || pmode === "lines") && fat_lines) { gl.enableVertexAttribArray(obj.pointLoc); enabled.pointLoc = true; gl.vertexAttribPointer(obj.pointLoc, 2, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.pointofs); gl.enableVertexAttribArray(obj.nextLoc ); enabled.nextLoc = true; gl.vertexAttribPointer(obj.nextLoc, 3, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.nextofs); gl.uniform1f(obj.aspectLoc, this.vp.width/this.vp.height); gl.uniform1f(obj.lwdLoc, this.getMaterial(id, "lwd")/this.vp.height); } gl.vertexAttribPointer(this.posLoc, 3, gl.FLOAT, false, 4*obj.vOffsets.stride, 4*obj.vOffsets.vofs); gl.drawElements(gl[mode], count, obj.index_uint ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT, 0); this.disableArrays(obj, enabled); } if (typeof obj.polygon_offset !== "undefined") gl.disable(gl.POLYGON_OFFSET_FILL); }; /** * Draw the background for a subscene * @param { number } id - id of background object * @param { number } subsceneid - id of subscene */ rglwidgetClass.prototype.drawBackground = function(id, subsceneid) { var gl = this.gl || this.initGL(), obj = this.getObj(id), bg, i; if (!obj.initialized) this.initObj(id); if (obj.colors.length) { bg = obj.colors[0]; gl.clearColor(bg[0], bg[1], bg[2], bg[3]); gl.depthMask(true); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); } if (typeof obj.quad !== "undefined") { this.prMatrix.makeIdentity(); this.mvMatrix.makeIdentity(); gl.disable(gl.BLEND); gl.disable(gl.DEPTH_TEST); gl.depthMask(false); for (i=0; i < obj.quad.length; i++) this.drawObj(obj.quad[i], subsceneid); } }; /** * Draw a subscene * @param { number } subsceneid - id of subscene * @param { boolean } opaquePass - is this the opaque drawing pass? */ rglwidgetClass.prototype.drawSubscene = function(subsceneid, opaquePass) { var gl = this.gl || this.initGL(), sub = this.getObj(subsceneid), objects = this.scene.objects, subids = sub.objects, subscene_has_faces = false, subscene_needs_sorting = false, flags, i, obj; if (sub.par3d.skipRedraw) return; for (i=0; i < subids.length; i++) { obj = objects[subids[i]]; flags = obj.flags; if (typeof flags !== "undefined") { subscene_has_faces |= (flags & this.f_is_lit) & !(flags & this.f_fixed_quads); obj.is_transparent = (flags & this.f_is_transparent) || obj.someHidden; subscene_needs_sorting |= (flags & this.f_depth_sort) || obj.is_transparent; } } this.setViewport(subsceneid); if (typeof sub.backgroundId !== "undefined" && opaquePass) this.drawBackground(sub.backgroundId, subsceneid); if (subids.length) { this.setprMatrix(subsceneid); this.setmvMatrix(subsceneid); if (subscene_has_faces) { this.setnormMatrix(subsceneid); if ((sub.flags & this.f_sprites_3d) && typeof sub.spriteNormmat === "undefined") { sub.spriteNormmat = new CanvasMatrix4(this.normMatrix); } } if (subscene_needs_sorting) this.setprmvMatrix(); var clipids = sub.clipplanes; if (typeof clipids === "undefined") { console.warn("bad clipids"); } if (clipids.length > 0) { this.invMatrix = new CanvasMatrix4(this.mvMatrix); this.invMatrix.invert(); for (i = 0; i < clipids.length; i++) this.drawObj(clipids[i], subsceneid); } subids = sub.opaque.concat(sub.transparent); if (opaquePass) { gl.enable(gl.DEPTH_TEST); gl.depthMask(true); gl.disable(gl.BLEND); for (i = 0; i < subids.length; i++) { if (!this.getObj(subids[i]).is_transparent) this.drawObj(subids[i], subsceneid); } } else { gl.depthMask(false); gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE); gl.enable(gl.BLEND); for (i = 0; i < subids.length; i++) { if (this.getObj(subids[i]).is_transparent) this.drawObj(subids[i], subsceneid); } } subids = sub.subscenes; for (i = 0; i < subids.length; i++) { this.drawSubscene(subids[i], opaquePass); } } }; /** * Respond to brush change */ rglwidgetClass.prototype.selectionChanged = function() { var i, j, k, id, subid = this.select.subscene, subscene, objids, obj, p1 = this.select.region.p1, p2 = this.select.region.p2, filter, selection = [], handle, keys, xmin, x, xmax, ymin, y, ymax, z, v, someHidden; if (!subid) return; subscene = this.getObj(subid); objids = subscene.objects; filter = this.scene.crosstalk.filter; this.setmvMatrix(subid); this.setprMatrix(subid); this.setprmvMatrix(); xmin = Math.min(p1.x, p2.x); xmax = Math.max(p1.x, p2.x); ymin = Math.min(p1.y, p2.y); ymax = Math.max(p1.y, p2.y); for (i = 0; i < objids.length; i++) { id = objids[i]; j = this.scene.crosstalk.id.indexOf(id); if (j >= 0) { keys = this.scene.crosstalk.key[j]; obj = this.getObj(id); someHidden = false; for (k = 0; k < keys.length; k++) { if (filter && filter.indexOf(keys[k]) < 0) { someHidden = true; continue; } v = [].concat(obj.vertices[k]).concat(1.0); v = this.multVM(v, this.prmvMatrix); x = v[0]/v[3]; y = v[1]/v[3]; z = v[2]/v[3]; if (xmin <= x && x <= xmax && ymin <= y && y <= ymax && -1.0 <= z && z <= 1.0) { selection.push(keys[k]); } else someHidden = true; } obj.someHidden = someHidden && (filter || selection.length); obj.initialized = false; /* Who should we notify? Only shared data in the current subscene, or everyone? */ if (!this.equalArrays(selection, this.scene.crosstalk.selection)) { handle = this.scene.crosstalk.sel_handle[j]; handle.set(selection, {rglSubsceneId: this.select.subscene}); } } } }; /** * Respond to selection or filter change from crosstalk * @param { Object } event - crosstalk event * @param { boolean } filter - filter or selection? */ rglwidgetClass.prototype.selection = function(event, filter) { var i, j, ids, obj, keys, crosstalk = this.scene.crosstalk, selection, someHidden; // Record the message and find out if this event makes some objects have mixed values: crosstalk = this.scene.crosstalk; if (filter) { filter = crosstalk.filter = event.value; selection = crosstalk.selection; } else { selection = crosstalk.selection = event.value; filter = crosstalk.filter; } ids = crosstalk.id; for (i = 0; i < ids.length ; i++) { obj = this.getObj(ids[i]); obj.initialized = false; keys = crosstalk.key[i]; someHidden = false; for (j = 0; j < keys.length && !someHidden; j++) { if ((filter && filter.indexOf(keys[j]) < 0) || (selection.length && selection.indexOf(keys[j]) < 0)) someHidden = true; } obj.someHidden = someHidden; } this.drawScene(); }; /** * Clear the selection brush * @param { number } except - Subscene that should ignore this request */ rglwidgetClass.prototype.clearBrush = function(except) { if (this.select.subscene != except) { this.select.state = "inactive"; this.delFromSubscene(this.scene.brushId, this.select.subscene); } this.drawScene(); }; /** * Compute mouse coordinates relative to current canvas * @returns { Object } * @param { Object } event - event object from mouse click */ rglwidgetClass.prototype.relMouseCoords = function(event) { var totalOffsetX = 0, totalOffsetY = 0, currentElement = this.canvas; do { totalOffsetX += currentElement.offsetLeft; totalOffsetY += currentElement.offsetTop; currentElement = currentElement.offsetParent; } while(currentElement); var canvasX = event.pageX - totalOffsetX, canvasY = event.pageY - totalOffsetY; return {x:canvasX, y:canvasY}; }; /** * Set mouse handlers for the scene */ rglwidgetClass.prototype.setMouseHandlers = function() { var self = this, activeSubscene, handler, handlers = {}, drag = 0; handlers.rotBase = 0; this.screenToVector = function(x, y) { var viewport = this.getObj(activeSubscene).par3d.viewport, width = viewport.width*this.canvas.width, height = viewport.height*this.canvas.height, radius = Math.max(width, height)/2.0, cx = width/2.0, cy = height/2.0, px = (x-cx)/radius, py = (y-cy)/radius, plen = Math.sqrt(px*px+py*py); if (plen > 1.e-6) { px = px/plen; py = py/plen; } var angle = (Math.SQRT2 - plen)/Math.SQRT2*Math.PI/2, z = Math.sin(angle), zlen = Math.sqrt(1.0 - z*z); px = px * zlen; py = py * zlen; return [px, py, z]; }; handlers.trackballdown = function(x,y) { var activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), i, l = activeModel.par3d.listeners; handlers.rotBase = this.screenToVector(x, y); this.saveMat = []; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.saveMat = new CanvasMatrix4(activeSub.par3d.userMatrix); } }; handlers.trackballmove = function(x,y) { var rotCurrent = this.screenToVector(x,y), rotBase = handlers.rotBase, dot = rotBase[0]*rotCurrent[0] + rotBase[1]*rotCurrent[1] + rotBase[2]*rotCurrent[2], angle = Math.acos( dot/this.vlen(rotBase)/this.vlen(rotCurrent) )*180.0/Math.PI, axis = this.xprod(rotBase, rotCurrent), objects = this.scene.objects, activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), l = activeModel.par3d.listeners, i; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.par3d.userMatrix.load(objects[l[i]].saveMat); activeSub.par3d.userMatrix.rotate(angle, axis[0], axis[1], axis[2]); } this.drawScene(); }; handlers.trackballend = 0; this.clamp = function(x, lo, hi) { return Math.max(lo, Math.min(x, hi)); }; this.screenToPolar = function(x,y) { var viewport = this.getObj(activeSubscene).par3d.viewport, width = viewport.width*this.canvas.width, height = viewport.height*this.canvas.height, r = Math.min(width, height)/2, dx = this.clamp(x - width/2, -r, r), dy = this.clamp(y - height/2, -r, r); return [Math.asin(dx/r), Math.asin(-dy/r)]; }; handlers.polardown = function(x,y) { var activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), i, l = activeModel.par3d.listeners; handlers.dragBase = this.screenToPolar(x, y); this.saveMat = []; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.saveMat = new CanvasMatrix4(activeSub.par3d.userMatrix); activeSub.camBase = [-Math.atan2(activeSub.saveMat.m13, activeSub.saveMat.m11), Math.atan2(activeSub.saveMat.m32, activeSub.saveMat.m22)]; } }; handlers.polarmove = function(x,y) { var dragCurrent = this.screenToPolar(x,y), activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), objects = this.scene.objects, l = activeModel.par3d.listeners, i, changepos = []; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); for (j=0; j<2; j++) changepos[j] = -(dragCurrent[j] - handlers.dragBase[j]); activeSub.par3d.userMatrix.makeIdentity(); activeSub.par3d.userMatrix.rotate(changepos[0]*180/Math.PI, 0,-1,0); activeSub.par3d.userMatrix.multRight(objects[l[i]].saveMat); activeSub.par3d.userMatrix.rotate(changepos[1]*180/Math.PI, -1,0,0); } this.drawScene(); }; handlers.polarend = 0; handlers.axisdown = function(x,y) { handlers.rotBase = this.screenToVector(x, this.canvas.height/2); var activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), i, l = activeModel.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.saveMat = new CanvasMatrix4(activeSub.par3d.userMatrix); } }; handlers.axismove = function(x,y) { var rotCurrent = this.screenToVector(x, this.canvas.height/2), rotBase = handlers.rotBase, angle = (rotCurrent[0] - rotBase[0])*180/Math.PI, rotMat = new CanvasMatrix4(); rotMat.rotate(angle, handlers.axis[0], handlers.axis[1], handlers.axis[2]); var activeSub = this.getObj(activeSubscene), activeModel = this.getObj(this.useid(activeSub.id, "model")), i, l = activeModel.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.par3d.userMatrix.load(activeSub.saveMat); activeSub.par3d.userMatrix.multLeft(rotMat); } this.drawScene(); }; handlers.axisend = 0; handlers.y0zoom = 0; handlers.zoom0 = 0; handlers.zoomdown = function(x, y) { var activeSub = this.getObj(activeSubscene), activeProjection = this.getObj(this.useid(activeSub.id, "projection")), i, l = activeProjection.par3d.listeners; handlers.y0zoom = y; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.zoom0 = Math.log(activeSub.par3d.zoom); } }; handlers.zoommove = function(x, y) { var activeSub = this.getObj(activeSubscene), activeProjection = this.getObj(this.useid(activeSub.id, "projection")), i, l = activeProjection.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.par3d.zoom = Math.exp(activeSub.zoom0 + (y-handlers.y0zoom)/this.canvas.height); } this.drawScene(); }; handlers.zoomend = 0; handlers.y0fov = 0; handlers.fovdown = function(x, y) { handlers.y0fov = y; var activeSub = this.getObj(activeSubscene), activeProjection = this.getObj(this.useid(activeSub.id, "projection")), i, l = activeProjection.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.fov0 = activeSub.par3d.FOV; } }; handlers.fovmove = function(x, y) { var activeSub = this.getObj(activeSubscene), activeProjection = this.getObj(this.useid(activeSub.id, "projection")), i, l = activeProjection.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = this.getObj(l[i]); activeSub.par3d.FOV = Math.max(1, Math.min(179, activeSub.fov0 + 180*(y-handlers.y0fov)/this.canvas.height)); } this.drawScene(); }; handlers.fovend = 0; handlers.selectingdown = function(x, y) { var viewport = this.getObj(activeSubscene).par3d.viewport, width = viewport.width*this.canvas.width, height = viewport.height*this.canvas.height, p = {x: 2.0*x/width - 1.0, y: 2.0*y/height - 1.0}; this.select.region = {p1: p, p2: p}; if (this.select.subscene && this.select.subscene != activeSubscene) this.delFromSubscene(this.scene.brushId, this.select.subscene); this.select.subscene = activeSubscene; this.addToSubscene(this.scene.brushId, activeSubscene); this.select.state = "changing"; if (typeof this.scene.brushId !== "undefined") this.getObj(this.scene.brushId).initialized = false; this.drawScene(); }; handlers.selectingmove = function(x, y) { var viewport = this.getObj(activeSubscene).par3d.viewport, width = viewport.width*this.canvas.width, height = viewport.height*this.canvas.height; if (this.select.state === "inactive") return; this.select.region.p2 = {x: 2.0*x/width - 1.0, y: 2.0*y/height - 1.0}; if (typeof this.scene.brushId !== "undefined") this.getObj(this.scene.brushId).initialized = false; this.drawScene(); }; handlers.selectingend = 0; this.canvas.onmousedown = function ( ev ){ if (!ev.which) // Use w3c defns in preference to MS switch (ev.button) { case 0: ev.which = 1; break; case 1: case 4: ev.which = 2; break; case 2: ev.which = 3; } drag = ["left", "middle", "right"][ev.which-1]; var coords = self.relMouseCoords(ev); coords.y = self.canvas.height-coords.y; activeSubscene = self.whichSubscene(coords); var sub = self.getObj(activeSubscene), f; handler = sub.par3d.mouseMode[drag]; switch (handler) { case "xAxis": handler = "axis"; handlers.axis = [1.0, 0.0, 0.0]; break; case "yAxis": handler = "axis"; handlers.axis = [0.0, 1.0, 0.0]; break; case "zAxis": handler = "axis"; handlers.axis = [0.0, 0.0, 1.0]; break; } f = handlers[handler + "down"]; if (f) { coords = self.translateCoords(activeSubscene, coords); f.call(self, coords.x, coords.y); ev.preventDefault(); } else console.warn("Mouse handler '" + handler + "' is not implemented."); }; this.canvas.onmouseup = function ( ev ){ if ( drag === 0 ) return; var f = handlers[handler + "end"]; if (f) { f.call(self); ev.preventDefault(); } drag = 0; }; this.canvas.onmouseout = this.canvas.onmouseup; this.canvas.onmousemove = function ( ev ) { if ( drag === 0 ) return; var f = handlers[handler + "move"]; if (f) { var coords = self.relMouseCoords(ev); coords.y = self.canvas.height - coords.y; coords = self.translateCoords(activeSubscene, coords); f.call(self, coords.x, coords.y); } }; handlers.wheelHandler = function(ev) { var del = 1.02, i; if (ev.shiftKey) del = 1.002; var ds = ((ev.detail || ev.wheelDelta) > 0) ? del : (1 / del); if (typeof activeSubscene === "undefined") activeSubscene = self.scene.rootSubscene; var activeSub = self.getObj(activeSubscene), activeProjection = self.getObj(self.useid(activeSub.id, "projection")), l = activeProjection.par3d.listeners; for (i = 0; i < l.length; i++) { activeSub = self.getObj(l[i]); activeSub.par3d.zoom *= ds; } self.drawScene(); ev.preventDefault(); }; this.canvas.addEventListener("DOMMouseScroll", handlers.wheelHandler, false); this.canvas.addEventListener("mousewheel", handlers.wheelHandler, false); }; /** * Find a particular subscene by inheritance * @returns { number } id of subscene to use * @param { number } subsceneid - child subscene * @param { string } type - type of inheritance: "projection" or "model" */ rglwidgetClass.prototype.useid = function(subsceneid, type) { var sub = this.getObj(subsceneid); if (sub.embeddings[type] === "inherit") return(this.useid(sub.parent, type)); else return subsceneid; }; /** * Check whether point is in viewport of subscene * @returns {boolean} * @param { Object } coords - screen coordinates of point * @param { number } subsceneid - subscene to check */ rglwidgetClass.prototype.inViewport = function(coords, subsceneid) { var viewport = this.getObj(subsceneid).par3d.viewport, x0 = coords.x - viewport.x*this.canvas.width, y0 = coords.y - viewport.y*this.canvas.height; return 0 <= x0 && x0 <= viewport.width*this.canvas.width && 0 <= y0 && y0 <= viewport.height*this.canvas.height; }; /** * Find which subscene contains a point * @returns { number } subscene id * @param { Object } coords - coordinates of point */ rglwidgetClass.prototype.whichSubscene = function(coords) { var self = this, recurse = function(subsceneid) { var subscenes = self.getChildSubscenes(subsceneid), i, id; for (i=0; i < subscenes.length; i++) { id = recurse(subscenes[i]); if (typeof(id) !== "undefined") return(id); } if (self.inViewport(coords, subsceneid)) return(subsceneid); else return undefined; }, rootid = this.scene.rootSubscene, result = recurse(rootid); if (typeof(result) === "undefined") result = rootid; return result; }; /** * Translate from window coordinates to viewport coordinates * @returns { Object } translated coordinates * @param { number } subsceneid - which subscene to use? * @param { Object } coords - point to translate */ rglwidgetClass.prototype.translateCoords = function(subsceneid, coords) { var viewport = this.getObj(subsceneid).par3d.viewport; return {x: coords.x - viewport.x*this.canvas.width, y: coords.y - viewport.y*this.canvas.height}; }; /** * Initialize the sphere object */ rglwidgetClass.prototype.initSphere = function() { var verts = this.scene.sphereVerts, reuse = verts.reuse, result; if (typeof reuse !== "undefined") { var prev = document.getElementById(reuse).rglinstance.sphere; result = {values: prev.values, vOffsets: prev.vOffsets, it: prev.it}; } else result = {values: new Float32Array(this.flatten(this.cbind(this.transpose(verts.vb), this.transpose(verts.texcoords)))), it: new Uint16Array(this.flatten(this.transpose(verts.it))), vOffsets: {vofs:0, cofs:-1, nofs:-1, radofs:-1, oofs:-1, tofs:3, nextofs:-1, pointofs:-1, stride:5}}; result.sphereCount = result.it.length; this.sphere = result; }; /** * Set the vertices in the selection box object */ rglwidgetClass.prototype.initSelection = function(id) { if (typeof this.select.region === "undefined") return; var obj = this.getObj(id), width = this.canvas.width, height = this.canvas.height, p1 = this.select.region.p1, p2 = this.select.region.p2; obj.vertices = [[p1.x, p1.y, 0.0], [p2.x, p1.y, 0.0], [p2.x, p2.y, 0.0], [p1.x, p2.y, 0.0], [p1.x, p1.y, 0.0]]; }; /** * Do the gl part of initializing the sphere */ rglwidgetClass.prototype.initSphereGL = function() { var gl = this.gl || this.initGL(), sphere = this.sphere; if (gl.isContextLost()) return; sphere.buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, sphere.buf); gl.bufferData(gl.ARRAY_BUFFER, sphere.values, gl.STATIC_DRAW); sphere.ibuf = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sphere.ibuf); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphere.it, gl.STATIC_DRAW); return; }; /** * Initialize the DOM object * @param { Object } el - the DOM object * @param { Object } x - the scene data sent by JSON from R */ rglwidgetClass.prototype.initialize = function(el, x) { this.textureCanvas = document.createElement("canvas"); this.textureCanvas.style.display = "block"; this.scene = x; this.normMatrix = new CanvasMatrix4(); this.saveMat = {}; this.distance = null; this.posLoc = 0; this.colLoc = 1; if (el) { el.rglinstance = this; this.el = el; this.webGLoptions = el.rglinstance.scene.webGLoptions; this.initCanvas(); } if (typeof Shiny !== "undefined") { var self = this; Shiny.addCustomMessageHandler("shinyGetPar3d", function(message) { var i, param, subscene = self.getObj(message.subscene), parameters = [].concat(message.parameters), result = {tag: message.tag, subscene: message.subscene}; if (typeof subscene !== "undefined") { for (i = 0; i < parameters.length; i++) { param = parameters[i]; result[param] = subscene.par3d[param]; }; } else { console.log("subscene "+message.subscene+" undefined.") } Shiny.setInputValue("par3d:shinyPar3d", result, {priority: "event"}); }); Shiny.addCustomMessageHandler("shinySetPar3d", function(message) { var param = message.parameter, subscene = self.getObj(message.subscene); if (typeof subscene !== "undefined") { subscene.par3d[param] = message.value; subscene.initialized = false; self.drawScene(); } else { console.log("subscene "+message.subscene+" undefined.") } }) } }; /** * Restart the WebGL canvas */ rglwidgetClass.prototype.restartCanvas = function() { var newcanvas = document.createElement("canvas"), self = this; newcanvas.width = this.el.width; newcanvas.height = this.el.height; newcanvas.addEventListener("webglcontextrestored", this.onContextRestored, false); newcanvas.addEventListener("webglcontextlost", this.onContextLost, false); while (this.el.firstChild) { this.el.removeChild(this.el.firstChild); } this.el.appendChild(newcanvas); this.canvas = newcanvas; this.setMouseHandlers(); if (this.gl) Object.keys(this.scene.objects).forEach(function(key){ self.getObj(parseInt(key, 10)).texture = undefined; }); this.gl = null; }; /** * Initialize the WebGL canvas */ rglwidgetClass.prototype.initCanvas = function() { this.restartCanvas(); var objs = this.scene.objects, self = this; Object.keys(objs).forEach(function(key){ var id = parseInt(key, 10), obj = self.getObj(id); if (typeof obj.reuse !== "undefined") self.copyObj(id, obj.reuse); }); Object.keys(objs).forEach(function(key){ self.initSubscene(parseInt(key, 10)); }); this.setMouseHandlers(); this.initSphere(); this.onContextRestored = function(event) { self.initGL(); self.drawScene(); }; this.onContextLost = function(event) { if (!self.drawing) this.gl = null; event.preventDefault(); }; this.initGL0(); this.lazyLoadScene = function() { if (typeof self.slide === "undefined") self.slide = self.getSlide(); if (self.isInBrowserViewport()) { if (!self.gl || self.gl.isContextLost()) self.initGL(); self.drawScene(); } }; window.addEventListener("DOMContentLoaded", this.lazyLoadScene, false); window.addEventListener("load", this.lazyLoadScene, false); window.addEventListener("resize", this.lazyLoadScene, false); window.addEventListener("scroll", this.lazyLoadScene, false); this.slide = this.getSlide(); if (this.slide) { if (typeof this.slide.rgl === "undefined") this.slide.rgl = [this]; else this.slide.rgl.push(this); if (this.scene.context.rmarkdown) if (this.scene.context.rmarkdown === "ioslides_presentation") { this.slide.setAttribute("slideenter", "this.rgl.forEach(function(scene) { scene.lazyLoadScene.call(window);})"); } else if (this.scene.context.rmarkdown === "slidy_presentation") { // This method would also work in ioslides, but it gets triggered // something like 5 times per slide for every slide change, so // you'd need a quicker function than lazyLoadScene. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver, observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { self.slide.rgl.forEach(function(scene) { scene.lazyLoadScene.call(window); });});}); observer.observe(this.slide, { attributes: true, attributeFilter:["class"] }); } } }; /** * Start the writeWebGL scene. This is only used by writeWebGL; rglwidget has no debug element and does the drawing in rglwidget.js. */ rglwidgetClass.prototype.start = function() { if (typeof this.prefix !== "undefined") { this.debugelement = document.getElementById(this.prefix + "debug"); this.debug(""); } this.drag = 0; this.drawScene(); }; /** * Display a debug message * @param { string } msg - The message to display * @param { Object } [img] - Image to insert before message */ rglwidgetClass.prototype.debug = function(msg, img) { if (typeof this.debugelement !== "undefined" && this.debugelement !== null) { this.debugelement.innerHTML = msg; if (typeof img !== "undefined") { this.debugelement.insertBefore(img, this.debugelement.firstChild); } } else if (msg !== "") alert(msg); }; /** * Get the snapshot image of this scene * @returns { Object } The img DOM element */ rglwidgetClass.prototype.getSnapshot = function() { var img; if (typeof this.scene.snapshot !== "undefined") { img = document.createElement("img"); img.src = this.scene.snapshot; img.alt = "Snapshot"; } return img; }; /** * Initial test for WebGL */ rglwidgetClass.prototype.initGL0 = function() { if (!window.WebGLRenderingContext){ alert("Your browser does not support WebGL. See http://get.webgl.org"); return; } }; /** * If we are in an ioslides or slidy presentation, get the * DOM element of the current slide * @returns { Object } */ rglwidgetClass.prototype.getSlide = function() { var result = this.el, done = false; while (result && !done && this.scene.context.rmarkdown) { switch(this.scene.context.rmarkdown) { case "ioslides_presentation": if (result.tagName === "SLIDE") return result; break; case "slidy_presentation": if (result.tagName === "DIV" && result.classList.contains("slide")) return result; break; default: return null; } result = result.parentElement; } return null; }; /** * Is this scene visible in the browser? * @returns { boolean } */ rglwidgetClass.prototype.isInBrowserViewport = function() { var rect = this.canvas.getBoundingClientRect(), windHeight = (window.innerHeight || document.documentElement.clientHeight), windWidth = (window.innerWidth || document.documentElement.clientWidth); if (this.scene.context && this.scene.context.rmarkdown !== null) { if (this.slide) return (this.scene.context.rmarkdown === "ioslides_presentation" && this.slide.classList.contains("current")) || (this.scene.context.rmarkdown === "slidy_presentation" && !this.slide.classList.contains("hidden")); } return ( rect.top >= -windHeight && rect.left >= -windWidth && rect.bottom <= 2*windHeight && rect.right <= 2*windWidth); }; /** * Initialize WebGL * @returns { Object } the WebGL context */ rglwidgetClass.prototype.initGL = function() { var self = this; if (this.gl) { if (!this.drawing && this.gl.isContextLost()) this.restartCanvas(); else return this.gl; } // if (!this.isInBrowserViewport()) return; Return what??? At this point we know this.gl is null. this.canvas.addEventListener("webglcontextrestored", this.onContextRestored, false); this.canvas.addEventListener("webglcontextlost", this.onContextLost, false); this.gl = this.canvas.getContext("webgl", this.webGLoptions) || this.canvas.getContext("experimental-webgl", this.webGLoptions); this.index_uint = this.gl.getExtension("OES_element_index_uint"); var save = this.startDrawing(); this.initSphereGL(); Object.keys(this.scene.objects).forEach(function(key){ self.initObj(parseInt(key, 10)); }); this.stopDrawing(save); return this.gl; }; /** * Resize the display to match element * @param { Object } el - DOM element to match */ rglwidgetClass.prototype.resize = function(el) { this.canvas.width = el.width; this.canvas.height = el.height; }; /** * Draw the whole scene */ rglwidgetClass.prototype.drawScene = function() { var gl = this.gl || this.initGL(), wasDrawing = this.startDrawing(); if (!wasDrawing) { if (this.select.state !== "inactive") this.selectionChanged(); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.clearDepth(1.0); gl.clearColor(1,1,1,1); gl.depthMask(true); // Must be true before clearing depth buffer gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); this.drawSubscene(this.scene.rootSubscene, true); this.drawSubscene(this.scene.rootSubscene, false); } this.stopDrawing(wasDrawing); }; /** * Change the displayed subset * @param { Object } el - Element of the control; not used. * @param { Object } control - The subset control data. */ rglwidgetClass.prototype.subsetSetter = function(el, control) { if (typeof control.subscenes === "undefined" || control.subscenes === null) control.subscenes = this.scene.rootSubscene; var value = Math.round(control.value), subscenes = [].concat(control.subscenes), fullset = [].concat(control.fullset), i, j, entries, subsceneid, adds = [], deletes = [], ismissing = function(x) { return fullset.indexOf(x) < 0; }, tointeger = function(x) { return parseInt(x, 10); }; if (isNaN(value)) value = control.value = 0; if (control.accumulate) for (i=0; i <= value; i++) adds = adds.concat(control.subsets[i]); else adds = adds.concat(control.subsets[value]); deletes = fullset.filter(function(x) { return adds.indexOf(x) < 0; }); for (i = 0; i < subscenes.length; i++) { subsceneid = subscenes[i]; if (typeof this.getObj(subsceneid) === "undefined") this.alertOnce("typeof object is undefined"); for (j = 0; j < adds.length; j++) this.addToSubscene(adds[j], subsceneid); for (j = 0; j < deletes.length; j++) this.delFromSubscene(deletes[j], subsceneid); } }; /** * Change the requested property * @param { Object } el - Element of the control; not used. * @param { Object } control - The property setter control data. */ rglwidgetClass.prototype.propertySetter = function(el, control) { var value = control.value, values = [].concat(control.values), svals = [].concat(control.param), direct = values[0] === null, entries = [].concat(control.entries), ncol = entries.length, nrow = values.length/ncol, properties = this.repeatToLen(control.properties, ncol), objids = this.repeatToLen(control.objids, ncol), property, objid = objids[0], obj = this.getObj(objid), propvals, i, v1, v2, p, entry, gl, needsBinding, newprop, newid, getPropvals = function() { if (property === "userMatrix") return obj.par3d.userMatrix.getAsArray(); else if (property === "scale" || property === "FOV" || property === "zoom") return [].concat(obj.par3d[property]); else return [].concat(obj[property]); }; putPropvals = function(newvals) { if (newvals.length == 1) newvals = newvals[0]; if (property === "userMatrix") obj.par3d.userMatrix.load(newvals); else if (property === "scale" || property === "FOV" || property === "zoom") obj.par3d[property] = newvals; else obj[property] = newvals; }; if (direct && typeof value === "undefined") return; if (control.interp) { values = values.slice(0, ncol).concat(values). concat(values.slice(ncol*(nrow-1), ncol*nrow)); svals = [-Infinity].concat(svals).concat(Infinity); for (i = 1; i < svals.length; i++) { if (value <= svals[i]) { if (svals[i] === Infinity) p = 1; else p = (svals[i] - value)/(svals[i] - svals[i-1]); break; } } } else if (!direct) { value = Math.round(value); } for (j=0; j<entries.length; j++) { entry = entries[j]; newprop = properties[j]; newid = objids[j]; if (newprop !== property || newid != objid) { if (typeof property !== "undefined") putPropvals(propvals); property = newprop; objid = newid; obj = this.getObj(objid); propvals = getPropvals(); } if (control.interp) { v1 = values[ncol*(i-1) + j]; v2 = values[ncol*i + j]; this.setElement(propvals, entry, p*v1 + (1-p)*v2); } else if (!direct) { this.setElement(propvals, entry, values[ncol*value + j]); } else { this.setElement(propvals, entry, value[j]); } } putPropvals(propvals); needsBinding = []; for (j=0; j < entries.length; j++) { if (properties[j] === "values" && needsBinding.indexOf(objids[j]) === -1) { needsBinding.push(objids[j]); } } for (j=0; j < needsBinding.length; j++) { gl = this.gl || this.initGL(); obj = this.getObj(needsBinding[j]); gl.bindBuffer(gl.ARRAY_BUFFER, obj.buf); gl.bufferData(gl.ARRAY_BUFFER, obj.values, gl.STATIC_DRAW); } }; /** * Change the requested vertices * @param { Object } el - Element of the control; not used. * @param { Object } control - The vertext setter control data. */ rglwidgetClass.prototype.vertexSetter = function(el, control) { var svals = [].concat(control.param), j, k, p, a, propvals, stride, ofs, obj, entry, attrib, ofss = {x:"vofs", y:"vofs", z:"vofs", red:"cofs", green:"cofs", blue:"cofs", alpha:"cofs", radii:"radofs", nx:"nofs", ny:"nofs", nz:"nofs", ox:"oofs", oy:"oofs", oz:"oofs", ts:"tofs", tt:"tofs"}, pos = {x:0, y:1, z:2, red:0, green:1, blue:2, alpha:3,radii:0, nx:0, ny:1, nz:2, ox:0, oy:1, oz:2, ts:0, tt:1}, values = control.values, direct = values === null, ncol, interp = control.interp, vertices = [].concat(control.vertices), attributes = [].concat(control.attributes), value = control.value, newval, aliases, alias; ncol = Math.max(vertices.length, attributes.length); if (!ncol) return; vertices = this.repeatToLen(vertices, ncol); attributes = this.repeatToLen(attributes, ncol); if (direct) interp = false; /* JSON doesn't pass Infinity */ svals[0] = -Infinity; svals[svals.length - 1] = Infinity; for (j = 1; j < svals.length; j++) { if (value <= svals[j]) { if (interp) { if (svals[j] === Infinity) p = 1; else p = (svals[j] - value)/(svals[j] - svals[j-1]); } else { if (svals[j] - value > value - svals[j-1]) j = j - 1; } break; } } obj = this.getObj(control.objid); // First, make sure color attributes vary in original if (typeof obj.vOffsets !== "undefined") { varies = true; for (k = 0; k < ncol; k++) { attrib = attributes[k]; if (typeof attrib !== "undefined") { ofs = obj.vOffsets[ofss[attrib]]; if (ofs < 0) { switch(attrib) { case "alpha": case "red": case "green": case "blue": obj.colors = [obj.colors[0], obj.colors[0]]; break; } varies = false; } } } if (!varies) this.initObj(control.objid); } propvals = obj.values; aliases = obj.alias; if (typeof aliases === "undefined") aliases = []; for (k=0; k<ncol; k++) { if (direct) { newval = value; } else if (interp) { newval = p*values[j-1][k] + (1-p)*values[j][k]; } else { newval = values[j][k]; } attrib = attributes[k]; vertex = vertices[k]; alias = aliases[vertex]; if (obj.type === "planes" || obj.type === "clipplanes") { ofs = ["nx", "ny", "nz", "offset"].indexOf(attrib); if (ofs >= 0) { if (ofs < 3) { if (obj.normals[vertex][ofs] != newval) { // Assume no aliases here... obj.normals[vertex][ofs] = newval; obj.initialized = false; } } else { if (obj.offsets[vertex][0] != newval) { obj.offsets[vertex][0] = newval; obj.initialized = false; } } continue; } } // Not a plane setting... ofs = obj.vOffsets[ofss[attrib]]; if (ofs < 0) this.alertOnce("Attribute '"+attrib+"' not found in object "+control.objid); else { stride = obj.vOffsets.stride; ofs = ofs + pos[attrib]; entry = vertex*stride + ofs; propvals[entry] = newval; if (typeof alias !== "undefined") for (a = 0; a < alias.length; a++) propvals[alias[a]*stride + ofs] = newval; } } if (typeof obj.buf !== "undefined") { var gl = this.gl || this.initGL(); gl.bindBuffer(gl.ARRAY_BUFFER, obj.buf); gl.bufferData(gl.ARRAY_BUFFER, propvals, gl.STATIC_DRAW); } }; /** * Change the requested vertex properties by age * @param { Object } el - Element of the control; not used. * @param { Object } control - The age setter control data. */ rglwidgetClass.prototype.ageSetter = function(el, control) { var objids = [].concat(control.objids), nobjs = objids.length, time = control.value, births = [].concat(control.births), ages = [].concat(control.ages), steps = births.length, j = Array(steps), p = Array(steps), i, k, age, j0, propvals, stride, ofs, objid, obj, attrib, dim, varies, alias, aliases, a, d, attribs = ["colors", "alpha", "radii", "vertices", "normals", "origins", "texcoords", "x", "y", "z", "red", "green", "blue"], ofss = ["cofs", "cofs", "radofs", "vofs", "nofs", "oofs", "tofs", "vofs", "vofs", "vofs", "cofs", "cofs", "cofs"], dims = [3,1,1,3, 3,2,2, 1,1,1, 1,1,1], pos = [0,3,0,0, 0,0,0, 0,1,2, 0,1,2]; /* Infinity doesn't make it through JSON */ ages[0] = -Infinity; ages[ages.length-1] = Infinity; for (i = 0; i < steps; i++) { if (births[i] !== null) { // NA in R becomes null age = time - births[i]; for (j0 = 1; age > ages[j0]; j0++); if (ages[j0] == Infinity) p[i] = 1; else if (ages[j0] > ages[j0-1]) p[i] = (ages[j0] - age)/(ages[j0] - ages[j0-1]); else p[i] = 0; j[i] = j0; } } // First, make sure color attributes vary in original for (l = 0; l < nobjs; l++) { objid = objids[l]; obj = this.getObj(objid); varies = true; if (typeof obj.vOffsets === "undefined") continue; for (k = 0; k < attribs.length; k++) { attrib = control[attribs[k]]; if (typeof attrib !== "undefined") { ofs = obj.vOffsets[ofss[k]]; if (ofs < 0) { switch(attribs[k]) { case "colors": case "alpha": case "red": case "green": case "blue": obj.colors = [obj.colors[0], obj.colors[0]]; break; } varies = false; } } } if (!varies) this.initObj(objid); } for (l = 0; l < nobjs; l++) { objid = objids[l]; obj = this.getObj(objid); if (typeof obj.vOffsets === "undefined") continue; aliases = obj.alias; if (typeof aliases === "undefined") aliases = []; propvals = obj.values; stride = obj.vOffsets.stride; for (k = 0; k < attribs.length; k++) { attrib = control[attribs[k]]; if (typeof attrib !== "undefined") { ofs = obj.vOffsets[ofss[k]]; if (ofs >= 0) { dim = dims[k]; ofs = ofs + pos[k]; for (i = 0; i < steps; i++) { alias = aliases[i]; if (births[i] !== null) { for (d=0; d < dim; d++) { propvals[i*stride + ofs + d] = p[i]*attrib[dim*(j[i]-1) + d] + (1-p[i])*attrib[dim*j[i] + d]; if (typeof alias !== "undefined") for (a=0; a < alias.length; a++) propvals[alias[a]*stride + ofs + d] = propvals[i*stride + ofs + d]; } } } } else this.alertOnce("\'"+attribs[k]+"\' property not found in object "+objid); } } obj.values = propvals; if (typeof obj.buf !== "undefined") { gl = this.gl || this.initGL(); gl.bindBuffer(gl.ARRAY_BUFFER, obj.buf); gl.bufferData(gl.ARRAY_BUFFER, obj.values, gl.STATIC_DRAW); } } }; /** * Bridge to old style control * @param { Object } el - Element of the control; not used. * @param { Object } control - The bridge control data. */ rglwidgetClass.prototype.oldBridge = function(el, control) { var attrname, global = window[control.prefix + "rgl"]; if (global) for (attrname in global) this[attrname] = global[attrname]; window[control.prefix + "rgl"] = this; }; /** * Set up a player control * @param { Object } el - The player control element * @param { Object } control - The player data. */ rglwidgetClass.prototype.Player = function(el, control) { var self = this, components = [].concat(control.components), buttonLabels = [].concat(control.buttonLabels), Tick = function() { /* "this" will be a timer */ var i, nominal = this.value, slider = this.Slider, labels = this.outputLabels, output = this.Output, step; if (typeof slider !== "undefined" && nominal != slider.value) slider.value = nominal; if (typeof output !== "undefined") { step = Math.round((nominal - output.sliderMin)/output.sliderStep); if (labels !== null) { output.innerHTML = labels[step]; } else { step = step*output.sliderStep + output.sliderMin; output.innerHTML = step.toPrecision(output.outputPrecision); } } for (i=0; i < this.actions.length; i++) { this.actions[i].value = nominal; } self.applyControls(el, this.actions, false); self.drawScene(); }, OnSliderInput = function() { /* "this" will be the slider */ this.rgltimer.value = Number(this.value); this.rgltimer.Tick(); }, addSlider = function(min, max, step, value) { var slider = document.createElement("input"); slider.type = "range"; slider.min = min; slider.max = max; slider.step = step; slider.value = value; slider.oninput = OnSliderInput; slider.sliderActions = control.actions; slider.sliderScene = this; slider.className = "rgl-slider"; slider.id = el.id + "-slider"; el.rgltimer.Slider = slider; slider.rgltimer = el.rgltimer; el.appendChild(slider); }, addLabel = function(labels, min, step, precision) { var output = document.createElement("output"); output.sliderMin = min; output.sliderStep = step; output.outputPrecision = precision; output.className = "rgl-label"; output.id = el.id + "-label"; el.rgltimer.Output = output; el.rgltimer.outputLabels = labels; el.appendChild(output); }, addButton = function(which, label, active) { var button = document.createElement("input"), onclicks = {Reverse: function() { this.rgltimer.reverse();}, Play: function() { this.rgltimer.play(); this.value = this.rgltimer.enabled ? this.inactiveValue : this.activeValue; }, Slower: function() { this.rgltimer.slower(); }, Faster: function() { this.rgltimer.faster(); }, Reset: function() { this.rgltimer.reset(); }, Step: function() { this.rgltimer.step(); } }; button.rgltimer = el.rgltimer; button.type = "button"; button.value = label; button.activeValue = label; button.inactiveValue = active; if (which === "Play") button.rgltimer.PlayButton = button; button.onclick = onclicks[which]; button.className = "rgl-button"; button.id = el.id + "-" + which; el.appendChild(button); }; if (typeof control.reinit !== "undefined" && control.reinit !== null) { control.actions.reinit = control.reinit; } el.rgltimer = new rgltimerClass(Tick, control.start, control.interval, control.stop, control.step, control.value, control.rate, control.loop, control.actions); for (var i=0; i < components.length; i++) { switch(components[i]) { case "Slider": addSlider(control.start, control.stop, control.step, control.value); break; case "Label": addLabel(control.labels, control.start, control.step, control.precision); break; default: addButton(components[i], buttonLabels[i], control.pause); } } el.rgltimer.Tick(); }; /** * Apply all registered controls * @param { Object } el - DOM element of the control * @param { Object } x - List of actions to apply * @param { boolean } [draw=true] - Whether to redraw after applying */ rglwidgetClass.prototype.applyControls = function(el, x, draw) { var self = this, reinit = x.reinit, i, control, type; for (i = 0; i < x.length; i++) { control = x[i]; type = control.type; self[type](el, control); } if (typeof reinit !== "undefined" && reinit !== null) { reinit = [].concat(reinit); for (i = 0; i < reinit.length; i++) self.getObj(reinit[i]).initialized = false; } if (typeof draw === "undefined" || draw) self.drawScene(); }; /** * Handler for scene change * @param { Object } message - What sort of scene change to do? */ rglwidgetClass.prototype.sceneChangeHandler = function(message) { var self = document.getElementById(message.elementId).rglinstance, objs = message.objects, mat = message.material, root = message.rootSubscene, initSubs = message.initSubscenes, redraw = message.redrawScene, skipRedraw = message.skipRedraw, deletes, subs, allsubs = [], i,j; if (typeof message.delete !== "undefined") { deletes = [].concat(message.delete); if (typeof message.delfromSubscenes !== "undefined") subs = [].concat(message.delfromSubscenes); else subs = []; for (i = 0; i < deletes.length; i++) { for (j = 0; j < subs.length; j++) { self.delFromSubscene(deletes[i], subs[j]); } delete self.scene.objects[deletes[i]]; } } if (typeof objs !== "undefined") { Object.keys(objs).forEach(function(key){ key = parseInt(key, 10); self.scene.objects[key] = objs[key]; self.initObj(key); var obj = self.getObj(key), subs = [].concat(obj.inSubscenes), k; allsubs = allsubs.concat(subs); for (k = 0; k < subs.length; k++) self.addToSubscene(key, subs[k]); }); } if (typeof mat !== "undefined") { self.scene.material = mat; } if (typeof root !== "undefined") { self.scene.rootSubscene = root; } if (typeof initSubs !== "undefined") allsubs = allsubs.concat(initSubs); allsubs = self.unique(allsubs); for (i = 0; i < allsubs.length; i++) { self.initSubscene(allsubs[i]); } if (typeof skipRedraw !== "undefined") { root = self.getObj(self.scene.rootSubscene); root.par3d.skipRedraw = skipRedraw; } if (redraw) self.drawScene(); }; /** * Set mouse mode for a subscene * @param { string } mode - name of mode * @param { number } button - button number (1 to 3) * @param { number } subscene - subscene id number * @param { number } stayActive - if truthy, don't clear brush */ rglwidgetClass.prototype.setMouseMode = function(mode, button, subscene, stayActive) { var sub = this.getObj(subscene), which = ["left", "right", "middle"][button - 1]; if (!stayActive && sub.par3d.mouseMode[which] === "selecting") this.clearBrush(null); sub.par3d.mouseMode[which] = mode; }; /** * The class of an rgl timer object * @class */ /** * Construct an rgltimerClass object * @constructor * @param { function } Tick - action when timer fires * @param { number } startTime - nominal start time in seconds * @param { number } interval - seconds between updates * @param { number } stopTime - nominal stop time in seconds * @param { number } stepSize - nominal step size * @param { number } value - current nominal time * @param { number } rate - nominal units per second * @param { string } loop - "none", "cycle" or "oscillate" * @param { Object } actions - list of actions */ rgltimerClass = function(Tick, startTime, interval, stopTime, stepSize, value, rate, loop, actions) { this.enabled = false; this.timerId = 0; /** nominal start time in seconds */ this.startTime = startTime; /** current nominal time */ this.value = value; /** seconds between updates */ this.interval = interval; /** nominal stop time */ this.stopTime = stopTime; /** nominal step size */ this.stepSize = stepSize; /** nominal units per second */ this.rate = rate; /** "none", "cycle", or "oscillate" */ this.loop = loop; /** real world start time */ this.realStart = undefined; /** multiplier for fast-forward or reverse */ this.multiplier = 1; this.actions = actions; this.Tick = Tick; }; /** * Start playing timer object */ rgltimerClass.prototype.play = function() { if (this.enabled) { this.enabled = false; window.clearInterval(this.timerId); this.timerId = 0; return; } var tick = function(self) { var now = new Date(); self.value = self.multiplier*self.rate*(now - self.realStart)/1000 + self.startTime; self.forceToRange(); if (typeof self.Tick !== "undefined") { self.Tick(self.value); } }; this.realStart = new Date() - 1000*(this.value - this.startTime)/this.rate/this.multiplier; this.timerId = window.setInterval(tick, 1000*this.interval, this); this.enabled = true; }; /** * Force value into legal range */ rgltimerClass.prototype.forceToRange = function() { if (this.value > this.stopTime + this.stepSize/2 || this.value < this.startTime - this.stepSize/2) { if (!this.loop) { this.reset(); } else { var cycle = this.stopTime - this.startTime + this.stepSize, newval = (this.value - this.startTime) % cycle + this.startTime; if (newval < this.startTime) { newval += cycle; } this.realStart += (this.value - newval)*1000/this.multiplier/this.rate; this.value = newval; } } }; /** * Reset to start values */ rgltimerClass.prototype.reset = function() { this.value = this.startTime; this.newmultiplier(1); if (typeof this.Tick !== "undefined") { this.Tick(this.value); } if (this.enabled) this.play(); /* really pause... */ if (typeof this.PlayButton !== "undefined") this.PlayButton.value = "Play"; }; /** * Increase the multiplier to play faster */ rgltimerClass.prototype.faster = function() { this.newmultiplier(Math.SQRT2*this.multiplier); }; /** * Decrease the multiplier to play slower */ rgltimerClass.prototype.slower = function() { this.newmultiplier(this.multiplier/Math.SQRT2); }; /** * Change sign of multiplier to reverse direction */ rgltimerClass.prototype.reverse = function() { this.newmultiplier(-this.multiplier); }; /** * Set multiplier for play speed * @param { number } newmult - new value */ rgltimerClass.prototype.newmultiplier = function(newmult) { if (newmult != this.multiplier) { this.realStart += 1000*(this.value - this.startTime)/this.rate*(1/this.multiplier - 1/newmult); this.multiplier = newmult; } }; /** * Take one step */ rgltimerClass.prototype.step = function() { this.value += this.rate*this.multiplier; this.forceToRange(); if (typeof this.Tick !== "undefined") this.Tick(this.value); };</script> <div id="testgldiv" class="rglWebGL"></div> <script type="text/javascript"> var testgldiv = document.getElementById("testgldiv"), testglrgl = new rglwidgetClass(); testgldiv.width = 865; testgldiv.height = 577; testglrgl.initialize(testgldiv, {"material":{"color":"#000000","alpha":1,"lit":true,"ambient":"#000000","specular":"#FFFFFF","emission":"#000000","shininess":50,"smooth":true,"front":"filled","back":"filled","size":3,"lwd":1,"fog":false,"point_antialias":false,"line_antialias":false,"texture":null,"textype":"rgb","texmipmap":false,"texminfilter":"linear","texmagfilter":"linear","texenvmap":false,"depth_mask":true,"depth_test":"less","isTransparent":false,"polygon_offset":[0,0]},"rootSubscene":1,"objects":{"7":{"id":7,"type":"lines","material":{"lit":false},"vertices":[[0,0,0],[0,0,0],[0,0,0],[0,0,0]],"colors":[[0,0,0,1]],"centers":[[0,0,0],[0,0,0]],"ignoreExtent":false,"flags":64},"8":{"id":8,"type":"text","material":{"lit":false},"vertices":[[0,-1.339,-1.339]],"colors":[[0,0,0,1]],"texts":[[""]],"cex":[[1]],"adj":[[0.5,0.5]],"centers":[[0,-1.339,-1.339]],"family":[["sans"]],"font":[[1]],"ignoreExtent":true,"flags":2064},"9":{"id":9,"type":"text","material":{"lit":false},"vertices":[[-1.339,0,-1.339]],"colors":[[0,0,0,1]],"texts":[[""]],"cex":[[1]],"adj":[[0.5,0.5]],"centers":[[-1.339,0,-1.339]],"family":[["sans"]],"font":[[1]],"ignoreExtent":true,"flags":2064},"10":{"id":10,"type":"text","material":{"lit":false},"vertices":[[-1.339,-1.339,0]],"colors":[[0,0,0,1]],"texts":[[""]],"cex":[[1]],"adj":[[0.5,0.5]],"centers":[[-1.339,-1.339,0]],"family":[["sans"]],"font":[[1]],"ignoreExtent":true,"flags":2064},"11":{"id":11,"type":"linestrip","material":{"lit":false},"vertices":[[-0.7788277,0,0],[2.473378,0,0]],"colors":[[0.2,0.2,0.2,1]],"centers":[[-0.7788277,0,0],[2.473378,0,0]],"ignoreExtent":false,"flags":64},"12":{"id":12,"type":"linestrip","material":{"lit":false},"vertices":[[0,-1.644634,0],[0,1.606822,0]],"colors":[[0.2,0.2,0.2,1]],"centers":[[0,-1.644634,0],[0,1.606822,0]],"ignoreExtent":false,"flags":64},"13":{"id":13,"type":"linestrip","material":{"lit":false},"vertices":[[0,0,-1.281192],[0,0,1.850868]],"colors":[[0.2,0.2,0.2,1]],"centers":[[0,0,-1.281192],[0,0,1.850868]],"ignoreExtent":false,"flags":64},"14":{"id":14,"type":"text","material":{"lit":false},"vertices":[[2.5059,0,0]],"colors":[[0.2,0.2,0.2,1]],"texts":[["1"]],"cex":[[0.75]],"adj":[[0.5,0.5]],"centers":[[2.5059,0,0]],"family":[["sans"]],"font":[[1]],"ignoreExtent":false,"flags":2064},"15":{"id":15,"type":"text","material":{"lit":false},"vertices":[[0,1.639344,0]],"colors":[[0.2,0.2,0.2,1]],"texts":[["2"]],"cex":[[0.75]],"adj":[[0.5,0.5]],"centers":[[0,1.639344,0]],"family":[["sans"]],"font":[[1]],"ignoreExtent":false,"flags":2064},"16":{"id":16,"type":"text","material":{"lit":false},"vertices":[[0,0,1.88339]],"colors":[[0.2,0.2,0.2,1]],"texts":[["3"]],"cex":[[0.75]],"adj":[[0.5,0.5]],"centers":[[0,0,1.88339]],"family":[["sans"]],"font":[[1]],"ignoreExtent":false,"flags":2064},"17":{"id":17,"type":"spheres","material":{"size":1},"vertices":[[-0.4333834,-0.6223485,-0.2850499],[-0.2177181,-0.2347692,-0.8806528],[0.665268,-0.07835874,0.2153373],[0.4092646,-0.02318444,-0.2866023],[0.474683,0.2854346,0.506478],[0.7482444,-0.1212604,0.07011276],[0.336904,-0.3351045,0.03170852],[0.9328901,0.1105501,0.1494488],[0.6008044,0.04307472,-0.08984677],[-0.6606353,0.7912011,0.9998518],[0.27293,-0.3203493,-0.1777114],[2.473378,-0.008067753,0.4313042],[-0.770311,-1.617451,1.780411],[-0.7788277,-1.644634,1.850868],[-0.3959337,-0.5684778,0.08383505],[-0.3806105,0.1647924,-0.162438],[-0.3000353,-0.4577278,-0.4149454],[-0.3680306,-0.3393358,-0.4953465],[-0.4408947,0.347441,-0.01100023],[-0.3650306,1.009643,0.4517177],[0.9546675,-0.0291397,0.0497981],[0.03534794,-0.152137,-0.2518772],[0.3615047,0.4445557,0.1918351],[-0.1568246,0.02989114,-0.2586318],[-0.3190093,-0.5061285,-0.1749196],[-0.3201435,-0.3805877,-1.281192],[0.2334083,-0.01406026,-0.2985971],[-0.1110823,-0.3103715,-1.105845],[0.07996739,0.281319,0.1120577],[-0.4188232,-0.3331176,-0.5900382],[0.1429186,-0.09291594,-0.1867097],[-0.2459475,-0.3866448,-0.6189411],[0.1336774,-0.4362731,-0.1112518],[-0.3097742,0.2911142,-0.1706648],[0.1927814,-0.3225222,0.1087191],[0.2404687,0.3271886,0.2795125],[0.361784,0.2972989,0.2566656],[-0.2292628,1.35748,0.5376554],[0.5593314,-0.07086501,0.04190909],[0.9771137,-0.0473663,-0.01145966],[0.321409,-0.1877604,-0.744235],[0.7219759,-0.1599101,0.01667433],[0.5346487,-0.07776982,0.0713772],[-0.4233183,0.9840461,0.4332437],[0.05441485,-0.1503461,-0.5668369],[-0.6209775,1.606822,0.7098864],[1.31196,-0.08810889,0.3269909],[-0.392211,0.2593781,0.1185957],[0.4827842,-0.1111689,0.17007],[0.1057707,0.098425,-0.008317037],[-0.3082354,0.09682893,0.04509097],[2.473378,-0.008067753,0.4313042],[0.0741723,-0.2217391,-0.2525965],[2.103693,0.1071747,0.404696],[-0.1112372,-0.0142876,0.04108546],[0.3057032,-0.09114277,0.1658607],[-0.4317377,0.5215757,-0.09233195],[-0.5075614,-0.6119925,-0.0523226],[-0.453626,0.2300137,0.08086072],[-0.2818823,0.2871227,-0.1689026],[-0.7732919,-1.626965,1.805071],[1.016155,-0.1730533,0.1548269],[-0.4334913,-0.462498,-0.4250052]],"colors":[[0.4,0.4,1,1]],"radii":[[0.01336505]],"centers":[[-0.4333834,-0.6223485,-0.2850499],[-0.2177181,-0.2347692,-0.8806528],[0.665268,-0.07835874,0.2153373],[0.4092646,-0.02318444,-0.2866023],[0.474683,0.2854346,0.506478],[0.7482444,-0.1212604,0.07011276],[0.336904,-0.3351045,0.03170852],[0.9328901,0.1105501,0.1494488],[0.6008044,0.04307472,-0.08984677],[-0.6606353,0.7912011,0.9998518],[0.27293,-0.3203493,-0.1777114],[2.473378,-0.008067753,0.4313042],[-0.770311,-1.617451,1.780411],[-0.7788277,-1.644634,1.850868],[-0.3959337,-0.5684778,0.08383505],[-0.3806105,0.1647924,-0.162438],[-0.3000353,-0.4577278,-0.4149454],[-0.3680306,-0.3393358,-0.4953465],[-0.4408947,0.347441,-0.01100023],[-0.3650306,1.009643,0.4517177],[0.9546675,-0.0291397,0.0497981],[0.03534794,-0.152137,-0.2518772],[0.3615047,0.4445557,0.1918351],[-0.1568246,0.02989114,-0.2586318],[-0.3190093,-0.5061285,-0.1749196],[-0.3201435,-0.3805877,-1.281192],[0.2334083,-0.01406026,-0.2985971],[-0.1110823,-0.3103715,-1.105845],[0.07996739,0.281319,0.1120577],[-0.4188232,-0.3331176,-0.5900382],[0.1429186,-0.09291594,-0.1867097],[-0.2459475,-0.3866448,-0.6189411],[0.1336774,-0.4362731,-0.1112518],[-0.3097742,0.2911142,-0.1706648],[0.1927814,-0.3225222,0.1087191],[0.2404687,0.3271886,0.2795125],[0.361784,0.2972989,0.2566656],[-0.2292628,1.35748,0.5376554],[0.5593314,-0.07086501,0.04190909],[0.9771137,-0.0473663,-0.01145966],[0.321409,-0.1877604,-0.744235],[0.7219759,-0.1599101,0.01667433],[0.5346487,-0.07776982,0.0713772],[-0.4233183,0.9840461,0.4332437],[0.05441485,-0.1503461,-0.5668369],[-0.6209775,1.606822,0.7098864],[1.31196,-0.08810889,0.3269909],[-0.392211,0.2593781,0.1185957],[0.4827842,-0.1111689,0.17007],[0.1057707,0.098425,-0.008317037],[-0.3082354,0.09682893,0.04509097],[2.473378,-0.008067753,0.4313042],[0.0741723,-0.2217391,-0.2525965],[2.103693,0.1071747,0.404696],[-0.1112372,-0.0142876,0.04108546],[0.3057032,-0.09114277,0.1658607],[-0.4317377,0.5215757,-0.09233195],[-0.5075614,-0.6119925,-0.0523226],[-0.453626,0.2300137,0.08086072],[-0.2818823,0.2871227,-0.1689026],[-0.7732919,-1.626965,1.805071],[1.016155,-0.1730533,0.1548269],[-0.4334913,-0.462498,-0.4250052]],"ignoreExtent":false,"flags":3},"18":{"id":18,"type":"text","material":{"lit":false},"vertices":[[-0.4333834,-0.3784893,-0.128447],[-0.2177181,0.009089937,-0.7240499],[0.665268,0.1655004,0.3719403],[0.4092646,0.2206747,-0.1299993],[0.474683,0.5292938,0.663081],[0.7482444,0.1225988,0.2267157],[0.336904,-0.09124536,0.1883115],[0.9328901,0.3544093,0.3060518],[0.6008044,0.2869339,0.0667562],[-0.6606353,1.03506,1.156455],[0.27293,-0.0764901,-0.02110845],[2.473378,0.2357914,0.5879071],[-0.770311,-1.373592,1.937014],[-0.7788277,-1.400774,2.007471],[-0.3959337,-0.3246186,0.240438],[-0.3806105,0.4086516,-0.005835051],[-0.3000353,-0.2138687,-0.2583424],[-0.3680306,-0.09547663,-0.3387436],[-0.4408947,0.5913001,0.1456027],[-0.3650306,1.253502,0.6083207],[0.9546675,0.2147195,0.2064011],[0.03534794,0.09172216,-0.09527419],[0.3615047,0.6884149,0.3484381],[-0.1568246,0.2737503,-0.1020288],[-0.3190093,-0.2622693,-0.01831665],[-0.3201435,-0.1367285,-1.124589],[0.2334083,0.2297989,-0.1419941],[-0.1110823,-0.06651232,-0.9492419],[0.07996739,0.5251782,0.2686607],[-0.4188232,-0.08925841,-0.4334353],[0.1429186,0.1509432,-0.03010676],[-0.2459475,-0.1427857,-0.4623381],[0.1336774,-0.1924139,0.04535113],[-0.3097742,0.5349734,-0.01406187],[0.1927814,-0.07866304,0.2653221],[0.2404687,0.5710478,0.4361155],[0.361784,0.5411581,0.4132686],[-0.2292628,1.601339,0.6942584],[0.5593314,0.1729942,0.1985121],[0.9771137,0.1964929,0.1451433],[0.321409,0.05609873,-0.5876321],[0.7219759,0.08394902,0.1732773],[0.5346487,0.1660893,0.2279802],[-0.4233183,1.227905,0.5898466],[0.05441485,0.09351305,-0.4102339],[-0.6209775,1.850681,0.8664894],[1.31196,0.1557503,0.4835939],[-0.392211,0.5032372,0.2751987],[0.4827842,0.1326903,0.326673],[0.1057707,0.3422842,0.1482859],[-0.3082354,0.3406881,0.2016939],[2.473378,0.2357914,0.5879071],[0.0741723,0.02212002,-0.09599354],[2.103693,0.3510339,0.5612989],[-0.1112372,0.2295716,0.1976884],[0.3057032,0.1527164,0.3224637],[-0.4317377,0.7654349,0.06427103],[-0.5075614,-0.3681334,0.1042804],[-0.453626,0.4738729,0.2374637],[-0.2818823,0.5309819,-0.01229967],[-0.7732919,-1.383106,1.961674],[1.016155,0.07080588,0.3114299],[-0.4334913,-0.2186389,-0.2684022]],"colors":[[0,0,1,1]],"texts":[["amazing"],["bar"],["bathroom"],["bed"],["booked"],["breakfast"],["city"],["clean"],["close"],["club"],["comfortable"],["cross"],["crown"],["crystal"],["dear"],["enjoyed"],["experience"],["fantastic"],["feedback"],["forward"],["free"],["friendly"],["good"],["great"],["hear"],["hello"],["helpful"],["hi"],["hotel"],["leave"],["location"],["lovely"],["melbourne"],["much"],["my"],["night"],["nights"],["noise"],["north"],["old"],["park"],["parking"],["place"],["positive"],["really"],["receive"],["reception"],["review"],["room"],["rooms"],["service"],["southern"],["staff"],["station"],["stay"],["stayed"],["taking"],["team"],["thank"],["time"],["towers"],["walk"],["wonderful"]],"cex":[[0.95]],"adj":[[0.5,0.5]],"centers":[[-0.4333834,-0.3784893,-0.128447],[-0.2177181,0.009089937,-0.7240499],[0.665268,0.1655004,0.3719403],[0.4092646,0.2206747,-0.1299993],[0.474683,0.5292938,0.663081],[0.7482444,0.1225988,0.2267157],[0.336904,-0.09124536,0.1883115],[0.9328901,0.3544093,0.3060518],[0.6008044,0.2869339,0.0667562],[-0.6606353,1.03506,1.156455],[0.27293,-0.0764901,-0.02110845],[2.473378,0.2357914,0.5879071],[-0.770311,-1.373592,1.937014],[-0.7788277,-1.400774,2.007471],[-0.3959337,-0.3246186,0.240438],[-0.3806105,0.4086516,-0.005835051],[-0.3000353,-0.2138687,-0.2583424],[-0.3680306,-0.09547663,-0.3387436],[-0.4408947,0.5913001,0.1456027],[-0.3650306,1.253502,0.6083207],[0.9546675,0.2147195,0.2064011],[0.03534794,0.09172216,-0.09527419],[0.3615047,0.6884149,0.3484381],[-0.1568246,0.2737503,-0.1020288],[-0.3190093,-0.2622693,-0.01831665],[-0.3201435,-0.1367285,-1.124589],[0.2334083,0.2297989,-0.1419941],[-0.1110823,-0.06651232,-0.9492419],[0.07996739,0.5251782,0.2686607],[-0.4188232,-0.08925841,-0.4334353],[0.1429186,0.1509432,-0.03010676],[-0.2459475,-0.1427857,-0.4623381],[0.1336774,-0.1924139,0.04535113],[-0.3097742,0.5349734,-0.01406187],[0.1927814,-0.07866304,0.2653221],[0.2404687,0.5710478,0.4361155],[0.361784,0.5411581,0.4132686],[-0.2292628,1.601339,0.6942584],[0.5593314,0.1729942,0.1985121],[0.9771137,0.1964929,0.1451433],[0.321409,0.05609873,-0.5876321],[0.7219759,0.08394902,0.1732773],[0.5346487,0.1660893,0.2279802],[-0.4233183,1.227905,0.5898466],[0.05441485,0.09351305,-0.4102339],[-0.6209775,1.850681,0.8664894],[1.31196,0.1557503,0.4835939],[-0.392211,0.5032372,0.2751987],[0.4827842,0.1326903,0.326673],[0.1057707,0.3422842,0.1482859],[-0.3082354,0.3406881,0.2016939],[2.473378,0.2357914,0.5879071],[0.0741723,0.02212002,-0.09599354],[2.103693,0.3510339,0.5612989],[-0.1112372,0.2295716,0.1976884],[0.3057032,0.1527164,0.3224637],[-0.4317377,0.7654349,0.06427103],[-0.5075614,-0.3681334,0.1042804],[-0.453626,0.4738729,0.2374637],[-0.2818823,0.5309819,-0.01229967],[-0.7732919,-1.383106,1.961674],[1.016155,0.07080588,0.3114299],[-0.4334913,-0.2186389,-0.2684022]],"family":[["sans"]],"font":[[1]],"ignoreExtent":false,"flags":2064},"19":{"id":19,"type":"triangles","material":{"size":1},"vertices":[[-0.315865,-0.1451017,-0.268511],[-0.303436,-0.1326727,-0.2529748],[-0.2910071,-0.1451017,-0.268511],[-0.2910071,-0.1451017,-0.268511],[-0.303436,-0.1326727,-0.2529748],[-0.2910071,-0.1202438,-0.268511],[-0.2910071,-0.1202438,-0.268511],[-0.303436,-0.1326727,-0.2529748],[-0.315865,-0.1202438,-0.268511],[-0.315865,-0.1202438,-0.268511],[-0.303436,-0.1326727,-0.2529748],[-0.315865,-0.1451017,-0.268511],[-0.2910071,-0.1451017,-0.268511],[-0.303436,-0.1326727,-0.2840472],[-0.315865,-0.1451017,-0.268511],[-0.2910071,-0.1202438,-0.268511],[-0.303436,-0.1326727,-0.2840472],[-0.2910071,-0.1451017,-0.268511],[-0.315865,-0.1202438,-0.268511],[-0.303436,-0.1326727,-0.2840472],[-0.2910071,-0.1202438,-0.268511],[-0.315865,-0.1451017,-0.268511],[-0.303436,-0.1326727,-0.2840472],[-0.315865,-0.1202438,-0.268511],[-0.2864055,0.7434006,0.2731932],[-0.2739765,0.7558296,0.2887293],[-0.2615476,0.7434006,0.2731932],[-0.2615476,0.7434006,0.2731932],[-0.2739765,0.7558296,0.2887293],[-0.2615476,0.7682585,0.2731932],[-0.2615476,0.7682585,0.2731932],[-0.2739765,0.7558296,0.2887293],[-0.2864055,0.7682585,0.2731932],[-0.2864055,0.7682585,0.2731932],[-0.2739765,0.7558296,0.2887293],[-0.2864055,0.7434006,0.2731932],[-0.2615476,0.7434006,0.2731932],[-0.2739765,0.7558296,0.257657],[-0.2864055,0.7434006,0.2731932],[-0.2615476,0.7682585,0.2731932],[-0.2739765,0.7558296,0.257657],[-0.2615476,0.7434006,0.2731932],[-0.2864055,0.7682585,0.2731932],[-0.2739765,0.7558296,0.257657],[-0.2615476,0.7682585,0.2731932],[-0.2864055,0.7434006,0.2731932],[-0.2739765,0.7558296,0.257657],[-0.2864055,0.7682585,0.2731932],[-0.3598633,-0.7187594,0.7289096],[-0.3474343,-0.7063304,0.7444457],[-0.3350054,-0.7187594,0.7289096],[-0.3350054,-0.7187594,0.7289096],[-0.3474343,-0.7063304,0.7444457],[-0.3350054,-0.6939015,0.7289096],[-0.3350054,-0.6939015,0.7289096],[-0.3474343,-0.7063304,0.7444457],[-0.3598633,-0.6939015,0.7289096],[-0.3598633,-0.6939015,0.7289096],[-0.3474343,-0.7063304,0.7444457],[-0.3598633,-0.7187594,0.7289096],[-0.3350054,-0.7187594,0.7289096],[-0.3474343,-0.7063304,0.7133734],[-0.3598633,-0.7187594,0.7289096],[-0.3350054,-0.6939015,0.7289096],[-0.3474343,-0.7063304,0.7133734],[-0.3350054,-0.7187594,0.7289096],[-0.3598633,-0.6939015,0.7289096],[-0.3474343,-0.7063304,0.7133734],[-0.3350054,-0.6939015,0.7289096],[-0.3598633,-0.7187594,0.7289096],[-0.3474343,-0.7063304,0.7133734],[-0.3598633,-0.6939015,0.7289096],[0.8172568,-0.01802547,0.1328279],[0.8296857,-0.005596545,0.1483641],[0.8421146,-0.01802547,0.1328279],[0.8421146,-0.01802547,0.1328279],[0.8296857,-0.005596545,0.1483641],[0.8421146,0.006832385,0.1328279],[0.8421146,0.006832385,0.1328279],[0.8296857,-0.005596545,0.1483641],[0.8172568,0.006832385,0.1328279],[0.8172568,0.006832385,0.1328279],[0.8296857,-0.005596545,0.1483641],[0.8172568,-0.01802547,0.1328279],[0.8421146,-0.01802547,0.1328279],[0.8296857,-0.005596545,0.1172918],[0.8172568,-0.01802547,0.1328279],[0.8421146,0.006832385,0.1328279],[0.8296857,-0.005596545,0.1172918],[0.8421146,-0.01802547,0.1328279],[0.8172568,0.006832385,0.1328279],[0.8296857,-0.005596545,0.1172918],[0.8421146,0.006832385,0.1328279],[0.8172568,-0.01802547,0.1328279],[0.8296857,-0.005596545,0.1172918],[0.8172568,0.006832385,0.1328279],[1.121352,-0.01565699,0.1739707],[1.133781,-0.003228056,0.1895069],[1.14621,-0.01565699,0.1739707],[1.14621,-0.01565699,0.1739707],[1.133781,-0.003228056,0.1895069],[1.14621,0.009200874,0.1739707],[1.14621,0.009200874,0.1739707],[1.133781,-0.003228056,0.1895069],[1.121352,0.009200874,0.1739707],[1.121352,0.009200874,0.1739707],[1.133781,-0.003228056,0.1895069],[1.121352,-0.01565699,0.1739707],[1.14621,-0.01565699,0.1739707],[1.133781,-0.003228056,0.1584345],[1.121352,-0.01565699,0.1739707],[1.14621,0.009200874,0.1739707],[1.133781,-0.003228056,0.1584345],[1.14621,-0.01565699,0.1739707],[1.121352,0.009200874,0.1739707],[1.133781,-0.003228056,0.1584345],[1.14621,0.009200874,0.1739707],[1.121352,-0.01565699,0.1739707],[1.133781,-0.003228056,0.1584345],[1.121352,0.009200874,0.1739707],[0.08133014,-0.09869421,-0.3308599],[0.09375907,-0.08626528,-0.3153237],[0.106188,-0.09869421,-0.3308599],[0.106188,-0.09869421,-0.3308599],[0.09375907,-0.08626528,-0.3153237],[0.106188,-0.07383636,-0.3308599],[0.106188,-0.07383636,-0.3308599],[0.09375907,-0.08626528,-0.3153237],[0.08133014,-0.07383636,-0.3308599],[0.08133014,-0.07383636,-0.3308599],[0.09375907,-0.08626528,-0.3153237],[0.08133014,-0.09869421,-0.3308599],[0.106188,-0.09869421,-0.3308599],[0.09375907,-0.08626528,-0.3463961],[0.08133014,-0.09869421,-0.3308599],[0.106188,-0.07383636,-0.3308599],[0.09375907,-0.08626528,-0.3463961],[0.106188,-0.09869421,-0.3308599],[0.08133014,-0.07383636,-0.3308599],[0.09375907,-0.08626528,-0.3463961],[0.106188,-0.07383636,-0.3308599],[0.08133014,-0.09869421,-0.3308599],[0.09375907,-0.08626528,-0.3463961],[0.08133014,-0.07383636,-0.3308599],[0.2671967,-0.0135947,0.001903044],[0.2796257,-0.00116577,0.01743921],[0.2920546,-0.0135947,0.001903044],[0.2920546,-0.0135947,0.001903044],[0.2796257,-0.00116577,0.01743921],[0.2920546,0.01126316,0.001903044],[0.2920546,0.01126316,0.001903044],[0.2796257,-0.00116577,0.01743921],[0.2671967,0.01126316,0.001903044],[0.2671967,0.01126316,0.001903044],[0.2796257,-0.00116577,0.01743921],[0.2671967,-0.0135947,0.001903044],[0.2920546,-0.0135947,0.001903044],[0.2796257,-0.00116577,-0.01363312],[0.2671967,-0.0135947,0.001903044],[0.2920546,0.01126316,0.001903044],[0.2796257,-0.00116577,-0.01363312],[0.2920546,-0.0135947,0.001903044],[0.2671967,0.01126316,0.001903044],[0.2796257,-0.00116577,-0.01363312],[0.2920546,0.01126316,0.001903044],[0.2671967,-0.0135947,0.001903044],[0.2796257,-0.00116577,-0.01363312],[0.2671967,0.01126316,0.001903044],[-0.1869955,-0.1875833,-0.5335813],[-0.1745666,-0.1751544,-0.5180451],[-0.1621376,-0.1875833,-0.5335813],[-0.1621376,-0.1875833,-0.5335813],[-0.1745666,-0.1751544,-0.5180451],[-0.1621376,-0.1627254,-0.5335813],[-0.1621376,-0.1627254,-0.5335813],[-0.1745666,-0.1751544,-0.5180451],[-0.1869955,-0.1627254,-0.5335813],[-0.1869955,-0.1627254,-0.5335813],[-0.1745666,-0.1751544,-0.5180451],[-0.1869955,-0.1875833,-0.5335813],[-0.1621376,-0.1875833,-0.5335813],[-0.1745666,-0.1751544,-0.5491174],[-0.1869955,-0.1875833,-0.5335813],[-0.1621376,-0.1627254,-0.5335813],[-0.1745666,-0.1751544,-0.5491174],[-0.1621376,-0.1875833,-0.5335813],[-0.1869955,-0.1627254,-0.5335813],[-0.1745666,-0.1751544,-0.5491174],[-0.1621376,-0.1627254,-0.5335813],[-0.1869955,-0.1875833,-0.5335813],[-0.1745666,-0.1751544,-0.5491174],[-0.1869955,-0.1627254,-0.5335813]],"normals":[[0,0.7808685,-0.6246954],[0,0.7808685,-0.6246954],[0,0.7808685,-0.6246954],[-0.7808692,0,-0.6246945],[-0.7808692,0,-0.6246945],[-0.7808692,0,-0.6246945],[0,-0.7808689,-0.624695],[0,-0.7808689,-0.624695],[0,-0.7808689,-0.624695],[0.7808685,0,-0.6246954],[0.7808685,0,-0.6246954],[0.7808685,0,-0.6246954],[0,0.7808685,0.6246954],[0,0.7808685,0.6246954],[0,0.7808685,0.6246954],[-0.7808692,0,0.6246945],[-0.7808692,0,0.6246945],[-0.7808692,0,0.6246945],[0,-0.7808689,0.624695],[0,-0.7808689,0.624695],[0,-0.7808689,0.624695],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0,0.7808685,-0.6246954],[0,0.7808685,-0.6246954],[0,0.7808685,-0.6246954],[-0.7808692,0,-0.6246945],[-0.7808692,0,-0.6246945],[-0.7808692,0,-0.6246945],[0,-0.7808685,-0.6246954],[0,-0.7808685,-0.6246954],[0,-0.7808685,-0.6246954],[0.7808685,0,-0.6246954],[0.7808685,0,-0.6246954],[0.7808685,0,-0.6246954],[0,0.7808685,0.6246954],[0,0.7808685,0.6246954],[0,0.7808685,0.6246954],[-0.7808692,0,0.6246945],[-0.7808692,0,0.6246945],[-0.7808692,0,0.6246945],[0,-0.7808685,0.6246954],[0,-0.7808685,0.6246954],[0,-0.7808685,0.6246954],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0,0.7808679,-0.6246962],[0,0.7808679,-0.6246962],[0,0.7808679,-0.6246962],[-0.7808687,0,-0.6246953],[-0.7808687,0,-0.6246953],[-0.7808687,0,-0.6246953],[0,-0.7808679,-0.6246962],[0,-0.7808679,-0.6246962],[0,-0.7808679,-0.6246962],[0.7808679,0,-0.6246961],[0.7808679,0,-0.6246961],[0.7808679,0,-0.6246961],[0,0.7808691,0.6246947],[0,0.7808691,0.6246947],[0,0.7808691,0.6246947],[-0.7808698,0,0.6246938],[-0.7808698,0,0.6246938],[-0.7808698,0,0.6246938],[0,-0.7808691,0.6246947],[0,-0.7808691,0.6246947],[0,-0.7808691,0.6246947],[0.7808691,0,0.6246946],[0.7808691,0,0.6246946],[0.7808691,0,0.6246946],[0,0.7808691,-0.6246948],[0,0.7808691,-0.6246948],[0,0.7808691,-0.6246948],[-0.7808703,0,-0.6246933],[-0.7808703,0,-0.6246933],[-0.7808703,0,-0.6246933],[0,-0.7808691,-0.6246948],[0,-0.7808691,-0.6246948],[0,-0.7808691,-0.6246948],[0.7808688,0,-0.624695],[0.7808688,0,-0.624695],[0.7808688,0,-0.624695],[0,0.7808687,0.6246951],[0,0.7808687,0.6246951],[0,0.7808687,0.6246951],[-0.7808699,0,0.6246935],[-0.7808699,0,0.6246935],[-0.7808699,0,0.6246935],[0,-0.7808687,0.6246951],[0,-0.7808687,0.6246951],[0,-0.7808687,0.6246951],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0.7808685,0,0.6246954],[0,0.780869,-0.6246948],[0,0.780869,-0.6246948],[0,0.780869,-0.6246948],[-0.7808703,0,-0.6246933],[-0.7808703,0,-0.6246933],[-0.7808703,0,-0.6246933],[0,-0.780869,-0.6246948],[0,-0.780869,-0.6246948],[0,-0.780869,-0.6246948],[0.7808703,0,-0.6246933],[0.7808703,0,-0.6246933],[0.7808703,0,-0.6246933],[0,0.7808687,0.6246951],[0,0.7808687,0.6246951],[0,0.7808687,0.6246951],[-0.7808699,0,0.6246936],[-0.7808699,0,0.6246936],[-0.7808699,0,0.6246936],[0,-0.7808687,0.6246951],[0,-0.7808687,0.6246951],[0,-0.7808687,0.6246951],[0.7808699,0,0.6246936],[0.7808699,0,0.6246936],[0.7808699,0,0.6246936],[0,0.7808687,-0.6246952],[0,0.7808687,-0.6246952],[0,0.7808687,-0.6246952],[-0.7808689,0,-0.624695],[-0.7808689,0,-0.624695],[-0.7808689,0,-0.624695],[0,-0.7808689,-0.624695],[0,-0.7808689,-0.624695],[0,-0.7808689,-0.624695],[0.7808687,0,-0.6246952],[0.7808687,0,-0.6246952],[0.7808687,0,-0.6246952],[0,0.7808687,0.6246952],[0,0.7808687,0.6246952],[0,0.7808687,0.6246952],[-0.7808689,0,0.624695],[-0.7808689,0,0.624695],[-0.7808689,0,0.624695],[0,-0.7808689,0.624695],[0,-0.7808689,0.624695],[0,-0.7808689,0.624695],[0.7808687,0,0.6246952],[0.7808687,0,0.6246952],[0.7808687,0,0.6246952],[0,0.7808688,-0.6246951],[0,0.7808688,-0.6246951],[0,0.7808688,-0.6246951],[-0.7808693,0,-0.6246944],[-0.7808693,0,-0.6246944],[-0.7808693,0,-0.6246944],[0,-0.7808688,-0.6246951],[0,-0.7808688,-0.6246951],[0,-0.7808688,-0.6246951],[0.7808685,0,-0.6246953],[0.7808685,0,-0.6246953],[0.7808685,0,-0.6246953],[0,0.7808688,0.6246951],[0,0.7808688,0.6246951],[0,0.7808688,0.6246951],[-0.7808693,0,0.6246944],[-0.7808693,0,0.6246944],[-0.7808693,0,0.6246944],[0,-0.7808688,0.6246951],[0,-0.7808688,0.6246951],[0,-0.7808688,0.6246951],[0.7808685,0,0.6246953],[0.7808685,0,0.6246953],[0.7808685,0,0.6246953],[0,0.7808683,-0.6246957],[0,0.7808683,-0.6246957],[0,0.7808683,-0.6246957],[-0.7808679,0,-0.6246961],[-0.7808679,0,-0.6246961],[-0.7808679,0,-0.6246961],[0,-0.7808683,-0.6246957],[0,-0.7808683,-0.6246957],[0,-0.7808683,-0.6246957],[0.7808683,0,-0.6246957],[0.7808683,0,-0.6246957],[0.7808683,0,-0.6246957],[0,0.7808695,0.6246942],[0,0.7808695,0.6246942],[0,0.7808695,0.6246942],[-0.7808691,0,0.6246946],[-0.7808691,0,0.6246946],[-0.7808691,0,0.6246946],[0,-0.7808695,0.6246942],[0,-0.7808695,0.6246942],[0,-0.7808695,0.6246942],[0.7808694,0,0.6246942],[0.7808694,0,0.6246942],[0.7808694,0,0.6246942]],"colors":[[1,0.4,0.4,1]],"centers":[[-0.303436,-0.1409587,-0.2633323],[-0.2951501,-0.1326727,-0.2633323],[-0.303436,-0.1243868,-0.2633323],[-0.311722,-0.1326727,-0.2633323],[-0.303436,-0.1409587,-0.2736897],[-0.2951501,-0.1326727,-0.2736897],[-0.303436,-0.1243868,-0.2736897],[-0.311722,-0.1326727,-0.2736897],[-0.2739766,0.7475436,0.2783719],[-0.2656906,0.7558296,0.2783719],[-0.2739765,0.7641156,0.2783719],[-0.2822625,0.7558296,0.2783719],[-0.2739765,0.7475436,0.2680145],[-0.2656906,0.7558296,0.2680145],[-0.2739766,0.7641156,0.2680145],[-0.2822625,0.7558296,0.2680145],[-0.3474344,-0.7146164,0.7340883],[-0.3391484,-0.7063305,0.7340883],[-0.3474344,-0.6980445,0.7340883],[-0.3557203,-0.7063304,0.7340883],[-0.3474344,-0.7146164,0.7237308],[-0.3391484,-0.7063304,0.7237308],[-0.3474344,-0.6980445,0.7237308],[-0.3557203,-0.7063305,0.7237308],[0.8296858,-0.0138825,0.1380067],[0.8379717,-0.005596545,0.1380067],[0.8296857,0.002689409,0.1380067],[0.8213999,-0.005596545,0.1380067],[0.8296857,-0.0138825,0.1276492],[0.8379717,-0.005596545,0.1276492],[0.8296858,0.002689409,0.1276492],[0.8213999,-0.005596545,0.1276492],[1.133781,-0.01151401,0.1791494],[1.142067,-0.003228057,0.1791494],[1.133781,0.005057897,0.1791494],[1.125495,-0.003228057,0.1791494],[1.133781,-0.01151401,0.168792],[1.142067,-0.003228057,0.168792],[1.133781,0.005057897,0.168792],[1.125495,-0.003228057,0.168792],[0.09375907,-0.09455124,-0.3256812],[0.102045,-0.08626529,-0.3256812],[0.09375906,-0.07797933,-0.3256812],[0.08547312,-0.08626529,-0.3256812],[0.09375906,-0.09455124,-0.3360386],[0.102045,-0.08626529,-0.3360386],[0.09375907,-0.07797933,-0.3360386],[0.08547312,-0.08626529,-0.3360386],[0.2796257,-0.009451725,0.007081765],[0.2879116,-0.001165771,0.007081765],[0.2796257,0.007120182,0.007081765],[0.2713397,-0.001165771,0.007081765],[0.2796257,-0.009451725,-0.003275677],[0.2879116,-0.001165771,-0.003275677],[0.2796257,0.007120182,-0.003275677],[0.2713397,-0.001165771,-0.003275677],[-0.1745666,-0.1834403,-0.5284026],[-0.1662806,-0.1751544,-0.5284026],[-0.1745666,-0.1668684,-0.5284026],[-0.1828525,-0.1751544,-0.5284026],[-0.1745666,-0.1834403,-0.53876],[-0.1662806,-0.1751544,-0.53876],[-0.1745666,-0.1668684,-0.53876],[-0.1828525,-0.1751544,-0.53876]],"ignoreExtent":false,"flags":3},"20":{"id":20,"type":"text","material":{"lit":false},"vertices":[[-0.303436,0.1111865,-0.111908],[-0.2739765,0.9996887,0.4297962],[-0.3474343,-0.4624712,0.8855125],[0.8296857,0.2382626,0.2894309],[1.133781,0.2406311,0.3305736],[0.09375907,0.1575939,-0.1742569],[0.2796257,0.2426934,0.158506],[-0.1745666,0.0687048,-0.3769783]],"colors":[[1,0,0,1]],"texts":[["Adelphi"],["Citiclub"],["CrownTowers"],["FlagstaffCity"],["HotelSophia"],["Larwill"],["Mercure"],["QT"]],"cex":[[0.95]],"adj":[[0.5,0.5]],"centers":[[-0.303436,0.1111865,-0.111908],[-0.2739765,0.9996887,0.4297962],[-0.3474343,-0.4624712,0.8855125],[0.8296857,0.2382626,0.2894309],[1.133781,0.2406311,0.3305736],[0.09375907,0.1575939,-0.1742569],[0.2796257,0.2426934,0.158506],[-0.1745666,0.0687048,-0.3769783]],"family":[["sans"]],"font":[[1]],"ignoreExtent":false,"flags":2064},"5":{"id":5,"type":"light","vertices":[[0,0,1]],"colors":[[1,1,1,1],[1,1,1,1],[1,1,1,1]],"viewpoint":true,"finite":false},"4":{"id":4,"type":"background","material":{"fog":true},"colors":[[0.2980392,0.2980392,0.2980392,1]],"centers":[[0,0,0]],"sphere":false,"fogtype":"none","flags":0},"6":{"id":6,"type":"background","material":{"lit":false,"back":"lines"},"colors":[[1,1,1,1]],"centers":[[0,0,0]],"sphere":false,"fogtype":"none","flags":0},"1":{"id":1,"type":"subscene","par3d":{"antialias":8,"FOV":30,"ignoreExtent":false,"listeners":1,"mouseMode":{"left":"trackball","right":"zoom","middle":"fov","wheel":"pull"},"observer":[0,0,11.2797],"modelMatrix":[[1,0,0,-0.8568536],[0,0.3420202,0.9396926,-0.3679106],[0,-0.9396926,0.3420202,-11.31109],[0,0,0,1]],"projMatrix":[[2.488034,0,0,0],[0,3.732051,0,0],[0,0,-3.863703,-40.66203],[0,0,-1,0]],"skipRedraw":false,"userMatrix":[[1,0,0,0],[0,0.3420201,0.9396926,0],[0,-0.9396926,0.3420201,0],[0,0,0,1]],"userProjection":[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],"scale":[1,1,1],"viewport":{"x":0,"y":0,"width":1,"height":1},"zoom":1,"bbox":[-0.7921928,2.5059,-1.657999,1.850681,-1.294557,2.007471],"windowRect":[100,100,964,676],"family":"sans","font":1,"cex":1,"useFreeType":false,"fontname":"TT Arial","maxClipPlanes":8,"glVersion":4.5,"activeSubscene":0},"embeddings":{"viewport":"replace","projection":"replace","model":"replace","mouse":"replace"},"objects":[6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,5],"subscenes":[],"flags":2643}},"snapshot":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA2AAAAJACAIAAABkK5JxAAAAHXRFWHRTb2Z0d2FyZQBSL1JHTCBwYWNrYWdlL2xpYnBuZ7GveO8AACAASURBVHic7d15jGR3YeDxgZD5J7JENNJmvYqU2V0piSL5r/4nUZKVFqFIUVqQQA4O2XFk0yYxeHB8BGMDDdjGxjiJHV8dIIAvHOzYoW0Qd3t8g8tgYwdItEmbgR4WkhCyIbeTtzVdM2/e7/2Oel3d03V9PhoNVa/eVT3tV19+r17VngoAABr2jHsHAACYLAIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBCBauGY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BAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAICAQAQAICAQAQAICEQAAAICEQCAgEAEACAgEAEACAhEAAACAhEAgIBABAAgIBABAAgIRAAAAgIRAIDA/wcmc29G83gfugAAAABJRU5ErkJggg==","width":865,"height":577,"sphereVerts":{"vb":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07465783,0.1464466,0.2126075,0.2705981,0.3181896,0.3535534,0.3753303,0.3826834,0.3753303,0.3535534,0.3181896,0.2705981,0.2126075,0.1464466,0.07465783,0,0,0.1379497,0.2705981,0.3928475,0.5,0.5879378,0.6532815,0.6935199,0.7071068,0.6935199,0.6532815,0.5879378,0.5,0.3928475,0.2705981,0.1379497,0,0,0.18024,0.3535534,0.51328,0.6532815,0.7681778,0.8535534,0.9061274,0.9238795,0.9061274,0.8535534,0.7681778,0.6532815,0.51328,0.3535534,0.18024,0,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,0.9807853,0.9238795,0.8314696,0.7071068,0.5555702,0.3826834,0.1950903,0,0,0.18024,0.3535534,0.51328,0.6532815,0.7681778,0.8535534,0.9061274,0.9238795,0.9061274,0.8535534,0.7681778,0.6532815,0.51328,0.3535534,0.18024,0,0,0.1379497,0.2705981,0.3928475,0.5,0.5879378,0.6532815,0.6935199,0.7071068,0.6935199,0.6532815,0.5879378,0.5,0.3928475,0.2705981,0.1379497,0,0,0.07465783,0.1464466,0.2126075,0.2705981,0.3181896,0.3535534,0.3753303,0.3826834,0.3753303,0.3535534,0.3181896,0.2705981,0.2126075,0.1464466,0.07465783,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0,-0.07465783,-0.1464466,-0.2126075,-0.2705981,-0.3181896,-0.3535534,-0.3753303,-0.3826834,-0.3753303,-0.3535534,-0.3181896,-0.2705981,-0.2126075,-0.1464466,-0.07465783,-0,-0,-0.1379497,-0.2705981,-0.3928475,-0.5,-0.5879378,-0.6532815,-0.6935199,-0.7071068,-0.6935199,-0.6532815,-0.5879378,-0.5,-0.3928475,-0.2705981,-0.1379497,-0,-0,-0.18024,-0.3535534,-0.51328,-0.6532815,-0.7681778,-0.8535534,-0.9061274,-0.9238795,-0.9061274,-0.8535534,-0.7681778,-0.6532815,-0.51328,-0.3535534,-0.18024,-0,-0,-0.1950903,-0.3826834,-0.5555702,-0.7071068,-0.8314696,-0.9238795,-0.9807853,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,-0,-0,-0.18024,-0.3535534,-0.51328,-0.6532815,-0.7681778,-0.8535534,-0.9061274,-0.9238795,-0.9061274,-0.8535534,-0.7681778,-0.6532815,-0.51328,-0.3535534,-0.18024,-0,-0,-0.1379497,-0.2705981,-0.3928475,-0.5,-0.5879378,-0.6532815,-0.6935199,-0.7071068,-0.6935199,-0.6532815,-0.5879378,-0.5,-0.3928475,-0.2705981,-0.1379497,-0,-0,-0.07465783,-0.1464466,-0.2126075,-0.2705981,-0.3181896,-0.3535534,-0.3753303,-0.3826834,-0.3753303,-0.3535534,-0.3181896,-0.2705981,-0.2126075,-0.1464466,-0.07465783,-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],[-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1],[0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,0.9807853,0.9238795,0.8314696,0.7071068,0.5555702,0.3826834,0.1950903,0,0,0.18024,0.3535534,0.51328,0.6532815,0.7681778,0.8535534,0.9061274,0.9238795,0.9061274,0.8535534,0.7681778,0.6532815,0.51328,0.3535534,0.18024,0,0,0.1379497,0.2705981,0.3928475,0.5,0.5879378,0.6532815,0.6935199,0.7071068,0.6935199,0.6532815,0.5879378,0.5,0.3928475,0.2705981,0.1379497,0,0,0.07465783,0.1464466,0.2126075,0.2705981,0.3181896,0.3535534,0.3753303,0.3826834,0.3753303,0.3535534,0.3181896,0.2705981,0.2126075,0.1464466,0.07465783,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-0,-0.07465783,-0.1464466,-0.2126075,-0.2705981,-0.3181896,-0.3535534,-0.3753303,-0.3826834,-0.3753303,-0.3535534,-0.3181896,-0.2705981,-0.2126075,-0.1464466,-0.07465783,-0,-0,-0.1379497,-0.2705981,-0.3928475,-0.5,-0.5879378,-0.6532815,-0.6935199,-0.7071068,-0.6935199,-0.6532815,-0.5879378,-0.5,-0.3928475,-0.2705981,-0.1379497,-0,-0,-0.18024,-0.3535534,-0.51328,-0.6532815,-0.7681778,-0.8535534,-0.9061274,-0.9238795,-0.9061274,-0.8535534,-0.7681778,-0.6532815,-0.51328,-0.3535534,-0.18024,-0,-0,-0.1950903,-0.3826834,-0.5555702,-0.7071068,-0.8314696,-0.9238795,-0.9807853,-1,-0.9807853,-0.9238795,-0.8314696,-0.7071068,-0.5555702,-0.3826834,-0.1950903,-0,-0,-0.18024,-0.3535534,-0.51328,-0.6532815,-0.7681778,-0.8535534,-0.9061274,-0.9238795,-0.9061274,-0.8535534,-0.7681778,-0.6532815,-0.51328,-0.3535534,-0.18024,-0,-0,-0.1379497,-0.2705981,-0.3928475,-0.5,-0.5879378,-0.6532815,-0.6935199,-0.7071068,-0.6935199,-0.6532815,-0.5879378,-0.5,-0.3928475,-0.2705981,-0.1379497,-0,-0,-0.07465783,-0.1464466,-0.2126075,-0.2705981,-0.3181896,-0.3535534,-0.3753303,-0.3826834,-0.3753303,-0.3535534,-0.3181896,-0.2705981,-0.2126075,-0.1464466,-0.07465783,-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.07465783,0.1464466,0.2126075,0.2705981,0.3181896,0.3535534,0.3753303,0.3826834,0.3753303,0.3535534,0.3181896,0.2705981,0.2126075,0.1464466,0.07465783,0,0,0.1379497,0.2705981,0.3928475,0.5,0.5879378,0.6532815,0.6935199,0.7071068,0.6935199,0.6532815,0.5879378,0.5,0.3928475,0.2705981,0.1379497,0,0,0.18024,0.3535534,0.51328,0.6532815,0.7681778,0.8535534,0.9061274,0.9238795,0.9061274,0.8535534,0.7681778,0.6532815,0.51328,0.3535534,0.18024,0,0,0.1950903,0.3826834,0.5555702,0.7071068,0.8314696,0.9238795,0.9807853,1,0.9807853,0.9238795,0.8314696,0.7071068,0.5555702,0.3826834,0.1950903,0]],"it":[[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270],[17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271]],"material":[],"normals":null,"texcoords":[[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.0625,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.125,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.1875,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.25,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.3125,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.4375,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.5625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.625,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.6875,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.75,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.8125,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.875,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,0.9375,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],[0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1,0,0.0625,0.125,0.1875,0.25,0.3125,0.375,0.4375,0.5,0.5625,0.625,0.6875,0.75,0.8125,0.875,0.9375,1]],"meshColor":"vertices"},"context":{"shiny":false,"rmarkdown":"xaringan::moon_reader"},"crosstalk":{"key":[],"group":[],"id":[],"options":[]}}); testglrgl.prefix = "testgl"; </script> <p id="testgldebug"> You must enable Javascript to view this page properly.</p> <script>testglrgl.start();</script> --- # Critique of the Analysis - Together the first two dimensions only explain slightly more than half of the inertia (52.2%)<!--D--> -- + This suggests a large proportion of dependence is not explained by the plot<!--D--> -- - Counting the frequency of words can be problematic.<!--D--> -- + Consider *clean* v *not clean*.<!--D--> -- - Also some aspects of the analysis are quite crude. Why use top 20 words? Why not 100?<!--D--> -- + More words more difficult to visualise. --- # Summary - Main things to know<!--D--> -- + CA used for categorical data.<!--D--> -- + Used to visualise two variable with many categories.<!--D--> -- + Aim is to maximise proportion of explained inertia. <!--D--> -- + Know how can it be used in practice.