(随時更新します)
バージョン
6.0.541.0-w
https://www.geogebra.org/m/zg3be8kn
各種モードの区別
アプレット内JavaScriptで、location.hrefの戻り値を取得する。
作成画面:geogebra.org/classic/ [ id ]
ワークシート画面:geogebra.org/m/ [ id ]
編集画面:geogebra.org/material/edit/id/ [ id ]
埋め込み時:geogebra.org/material/iframe/id/ [ id ] /width/...
function getAppletType(){
var url = location.href;
var e1 = url.split('/')[3];
if(e1 == 'material'){e1 = url.split('/')[4];}
return e1;
}
グラフィックスビュー1を表すcanvasエレメントを取得する
document.getElementsByTagName('canvas')[0];
で取得可能。
※以下、グラフィックスビュー1のことを、「GV1」と略記する場合がある。
※ただし、GV1以外のビューを、GV1と同時に表示している場合には、上記ではうまく取得できないかもしれない。以下では、GV1以外のビューが表示されていないことを前提とする。
※webページ埋め込み時に、コンソールで、当該webページを対象フレームとして、上記getElementsByTagNameを実行しても、undefinedが返される。
対象フレームは、www.geogebra.orgの「false」を選択すべし。
埋め込み時の主な階層
iframe
#document
div class = 'wf-mathsans-n4-active wf-active'
body
div class = 'applet_container'
div class = 'applet_scaler ggbTransform'
div class = 'notranslate'
div class = 'GeoGebraFrame applet-unfocused jsloaded'
div class = 'gwt-SplitLayoutPanel splitterFixed'(1個目)
div class = 'gwt-SplitLayoutPanel splitterFixed'(2個目)
div class = 'ggbdockpanelhack'
div class = 'EuclidianPanel'
canvas
(参考)アプレットが格納されているiframeを取得する
function getGeoFrame(){
var iframeArr = document.getElementsByTagName('iframe');
for(var k=0; k<iframeArr.length; k++){
if(iframeArr[k].outerHTML.indexOf('geogebra.org/material/iframe/') != -1){
return iframeArr[k];
}
}
}
ワークシート画面の主な階層
編集画面の主な階層
ウインドウサイズとcanvas.width / height
アプレット作成画面:Corner[5]と、canvas.width, heightは一致
ワークシート画面:一致せず(ウインドウサイズが小さい場合、canvas.width, heightが可変)
編集画面:一致
埋め込み時:一致せず(ウインドウサイズが小さい場合、canvas.width, heightが可変)
ただし、タッチデバイスの場合、(ウインドウサイズが小さくなく、縮小の必要がない状態で)canvas.width, heightは、Corner[5]の各座標の2倍の値をもつ。
function getCanvasSize(){
var canvas = document.getElementsByTagName('canvas')[0];
var canvasWidth = canvas.width;
var styleWidth = canvas.style.width;
styleWidth = eval( styleWidth.slice( 0, styleWidth.indexOf('px') ) );
var canvasHeight = canvas.height;
var styleHeight = canvas.style.height;
styleHeight = eval( styleHeight.slice( 0, styleHeight.indexOf('px') ) );
return [canvasWidth, styleWidth, canvasHeight, styleHeight];
}
( (x(A) - x(Corner[4] + ( (x(Corner[2]) - x(Corner[1]) ) / (x(Corner[5]) + 2), (-(y(Corner[4]) - y(Corner[1]) ) ) / (y(Corner[5]) + 2) ) ) ) (x(Corner[5]) + 2) / (x(Corner[2]) - x(Corner[1] ) ), (y(Corner[4] + ( (x(Corner[2]) - x(Corner[1]) ) / (x(Corner[5]) + 2), (-(y(Corner[4]) - y(Corner[1]) ) ) / (y(Corner[5]) + 2) ) ) - y(A)) (y(Corner[5]) + 2) / (y(Corner[4]) - y(Corner[1]) ) )
Corner[4] + ( (w + 1) (x(Corner[2]) - x(Corner[1]) ) / (x(Corner[5]) + 2), -(h + 1) (y(Corner[4]) - y(Corner[1]) ) / (y(Corner[5]) + 2) )
右下隅の点
Corner[2] + ( (-(x(Corner[2] ) - x(Corner[1] ) ) ) / (x(Corner[5] ) + 2), (y(Corner[4]) - y(Corner[1]) ) / (y(Corner[5]) + 2) )
この点のピクセル座標は、Corner[5]と一致する。
ウインドウサイズ拡大/縮小時の値の変化
Corner[5]:作成画面のみ可変
右下隅の点のピクセル座標:作成画面のみ可変
canvas.width, canvas.height:すべてのモードで可変
canvas.style.width, canvas.style.height:作成画面のみ可変
canvasのattributes変更を監視する
function startObserve(){
var canvas = document.getElementsByTagName('canvas')[0];
var mo = new MutationObserver(function(records){
if(records){
var usiArr = [];
for(var k=0; k < records.length; k++){
usiArr[k] = records[k].attributeName;
}
if( usiArr.indexOf('width') != -1 || usiArr.indexOf('height') != -1 || usiArr.indexOf('style') != -1){
var currentCanvasSize = getCanvasSize();
console.log('now the sizeData of GraphicsView1 is ' + currentCanvasSize);
var usicanvas = document.getElementsByClassName('usicanvas')[0];
if(usicanvas){
usicanvas.width = currentCanvasSize[1];
usicanvas.height = currentCanvasSize[3];
clearCanvas(usicanvas);
drawCanvas(usicanvas);
}
}
}
});
var config = {attributes: true};
mo.observe(canvas, config);
}
function drawCanvas(canvas){
if (canvas.getContext) {
var context = canvas.getContext('2d');
context.fillStyle = 'rgba(' + [0, 0, 255, 0.3] + ')';
context.fillRect(0,0,250,200);
}
}
function clearCanvas(canvas){
if (canvas.getContext) {
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
}
}
GV1にdrawcanvasを実行しても、一瞬しか描画されない。
ウインドウサイズ拡大・縮小で、canvasのスケールが変わっても、描画結果自体もそれに合わせて拡大・縮小される。タッチデバイスでも同じ。
新たなcanvasの作成(方法A : 埋め込み時、編集画面にのみ有効)
一瞬しか描画されないのは困る。requestAnimationFrameで、フレームごとに描画命令を出しても良いが、処理が多く負荷がかかるので、避ける。
GV1と同じ大きさ、同じ位置に、新しいcanvas(以下ではusicanvasと呼ぶことにする)を設置して、そこに目的の描画を行う方向でいく。
function insertCanvas(){
var preCanvas = document.getElementsByTagName('canvas')[0];
var canvasWidth = preCanvas.width;
var canvasHeight = preCanvas.height;
var canvasCss = preCanvas.style.cssText;
preCanvas.parentNode.insertAdjacentHTML('afterend','<div style="position: absolute; right: 0px; bottom: -4px;"><canvas height=\"'+canvasHeight+'\" width=\"'+canvasWidth+'\" dir=\"ltr\" tabindex=\"10000\" class=\"usicanvas\">UsiCanvas</canvas></div>');
document.getElementsByClassName('usicanvas')[0].style.cssText = canvasCss;
}
埋め込み時には、ウインドウサイズ縮小に合わせて、GV1のcanvasのスケールが変わる。この場合には、それに合わせてusicanvasのスケールも変わるから、描画にズレは生じない。
ただし、usicanvas.width / height は、手動で値を更新しないと変わらない。
現在のwidth / height を取得したいときは、GV1のcanvasの方で測定すべし(attributes変更を監視)。
編集画面では、そもそもウインドウサイズにかかわらず、GV1のcanvasのスケールは変わらないから、描画のズレも、もちろん生じない。
※上記方法は、作成画面、ワークシート画面では、ズレが生じてしまう。試行錯誤の結果、すべてのモードにおいてズレが生じない、「方法B」を開発した(下記)。そこで、方法Aの開発は、いったんここで打ち切る。
新たなcanvasの作成(方法B : 全モードに完全対応)
function insertCanvasOnlyCanvasTag(){
var preCanvas = document.getElementsByTagName('canvas')[0];
var canvasWidth = preCanvas.style.width;
canvasWidth = eval( canvasWidth.slice( 0, canvasWidth.indexOf('px') ) );
var canvasHeight = preCanvas.style.height;
canvasHeight = eval( canvasHeight.slice( 0, canvasHeight.indexOf('px') ) );
preCanvas.insertAdjacentHTML('afterend','<canvas height=\"'+canvasHeight+'\" width=\"'+canvasWidth+'\" dir=\"ltr\" tabindex=\"10000\" class=\"usicanvas\" style=\"position: absolute; right: 0px; bottom: 0px;\">UsiCanvas</canvas>');
}
関数名どおり、canvasタグのみを挿入する。divタグは作らず、GV1のcanvasを包んでいるdivの子要素として、canvasを作成する。
style.position はabsoluteで、right, bottomともに0pxである。
この設定では、作成画面以外のモードで、描画のズレは起こらない。
作成画面では、いったん描画したのち、ウインドウサイズを変更すると、ズレが起こる。これは、他のモードと違い、作成画面では、GV1のcanvasにおけるcanvas.style.width および canvas.style.height が、ウインドウサイズの変更にあわせて変化するからだと思われる。
そこで、GV1のcanvasのattributesを監視して、canvas.style.width またはcanvas.style.height 変更のタイミングで、usicanvas.width, usicanvas.heightを、変更後の canvas.style.width, canvas.style.heightの値に揃えることにした。
なお、usicanvas.width / height をいじると、描画がリセットされるので(canvasの仕様のようだ)、いじるたびに描画をやり直す必要がある。
上記操作については、上記startObserve 関数内のコールバック関数の記述を参照。
usicanvasの表示・非表示
document.getElementsByClassName('usicanvas')[0].hidden = true; //非表示
document.getElementsByClassName('usicanvas')[0].hidden = false; //表示
ggbOnInit内で、usicanvasを作成して、そのままにしておくと、GV1上のGeoGebraオブジェクトに対するマウス操作が効かなかった。
そこで、usicanvas作成後は、ただちにusicanvasを非表示にして、マウス操作を可能にした(下記 ggbOnInit 関数参照)。
ボタン等でdrawCanvas 関数を実行したタイミングで、usicanvasを表示にすれば、以降のマウス操作は可能のようである。
したがって、アプレット起動時のみ、usicanvasは非表示にしておき、その後、GeoGebraオブジェクトのイベントハンドラから、usicanvasを表示にすれば、この問題は回避できる。
以上のまとめ。
function ggbOnInit() {
console.log( getAppletType() );
startObserve();
insertCanvasOnlyCanvasTag();
document.getElementsByClassName('usicanvas')[0].hidden = true;
console.log('ggbOnInit done.');
}
function getAppletType(){
var url = location.href;
var e1 = url.split('/')[3];
if(e1 == 'material'){e1 = url.split('/')[4];}
return e1;
}
function getCanvasSize(){
var canvas = document.getElementsByTagName('canvas')[0];
var canvasWidth = canvas.width;
var styleWidth = canvas.style.width;
styleWidth = eval( styleWidth.slice( 0, styleWidth.indexOf('px') ) );
var canvasHeight = canvas.height;
var styleHeight = canvas.style.height;
styleHeight = eval( styleHeight.slice( 0, styleHeight.indexOf('px') ) );
return [canvasWidth, styleWidth, canvasHeight, styleHeight];
}
function drawCanvas(canvas){
if (canvas.getContext) {
var context = canvas.getContext('2d');
context.fillStyle = 'rgba(' + [0, 0, 255, 0.3] + ')';
context.fillRect(0,0,250,200);
}
}
function clearCanvas(canvas){
if (canvas.getContext) {
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
}
}
function startObserve(){
var canvas = document.getElementsByTagName('canvas')[0];
var mo = new MutationObserver(function(records){
if(records){
var usiArr = [];
for(var k=0; k < records.length; k++){
usiArr[k] = records[k].attributeName;
}
if( usiArr.indexOf('width') != -1 || usiArr.indexOf('height') != -1 || usiArr.indexOf('style') != -1){
var currentCanvasSize = getCanvasSize();
console.log('now the sizeData of GraphicsView1 is ' + currentCanvasSize);
var usicanvas = document.getElementsByClassName('usicanvas')[0];
if(usicanvas){
usicanvas.width = currentCanvasSize[1];
usicanvas.height = currentCanvasSize[3];
clearCanvas(usicanvas);
drawCanvas(usicanvas);
}
}
}
});
var config = {attributes: true};
mo.observe(canvas, config);
}
function insertCanvasOnlyCanvasTag(){
var preCanvas = document.getElementsByTagName('canvas')[0];
var canvasWidth = preCanvas.style.width;
canvasWidth = eval( canvasWidth.slice( 0, canvasWidth.indexOf('px') ) );
var canvasHeight = preCanvas.style.height;
canvasHeight = eval( canvasHeight.slice( 0, canvasHeight.indexOf('px') ) );
preCanvas.insertAdjacentHTML('afterend','<canvas height=\"'+canvasHeight+'\" width=\"'+canvasWidth+'\" dir=\"ltr\" tabindex=\"10000\" class=\"usicanvas\" style=\"position: absolute; right: 0px; bottom: 0px;\">UsiCanvas</canvas>');
}
canvasのピクセルデータ(RGBA情報が格納された配列)を取得する
function getPixelArray(canvas){
var context = canvas.getContext('2d');
var styleWidth = canvas.style.width;
styleWidth = eval( styleWidth.slice( 0, styleWidth.indexOf('px') ) );
if(!styleWidth){styleWidth = canvas.width;}
var styleHeight = canvas.style.height;
styleHeight = eval( styleHeight.slice( 0, styleHeight.indexOf('px') ) );
if(!styleHeight){styleHeight = canvas.height;}
var imageData = context.getImageData(0, 0, styleWidth, styleHeight);
var pixelArray = imageData.data;
return pixelArray;
}
ピクセル通し番号→GV1における座標
ピクセル通し番号:GV1の左上を0として、横書きの要領で走査し、右下隅のwidth*height-1に至る。
function getCoordsFromPixelIndex(pixelIndex){
var canvas = document.getElementsByTagName('canvas')[0];
var styleWidth = canvas.style.width;
styleWidth = eval( styleWidth.slice( 0, styleWidth.indexOf('px') ) );
var styleHeight = canvas.style.height;
styleHeight = eval( styleHeight.slice( 0, styleHeight.indexOf('px') ) );
pixelIndex = pixelIndex % ( styleWidth * styleHeight );
var pixelX = pixelIndex % styleWidth;
var pixelY = Math.floor(pixelIndex / styleWidth);
var gvXleftUp = ggbApplet.getValue('x(Corner[4]) + ('+pixelX+' + 1) (x(Corner[2]) - x(Corner[1])) / (x(Corner[5]) + 2)');
var gvXrightDown = ggbApplet.getValue('x(Corner[4]) + ('+pixelX+' + 2) (x(Corner[2]) - x(Corner[1])) / (x(Corner[5]) + 2)');
var gvX = (gvXleftUp + gvXrightDown) / 2;
var gvYleftUp = ggbApplet.getValue('y(Corner[4]) - ('+pixelY+' + 1) (y(Corner[4]) - y(Corner[1])) / (y(Corner[5]) + 2)');
var gvYrightDown = ggbApplet.getValue('y(Corner[4]) - ('+pixelY+' + 2) (y(Corner[4]) - y(Corner[1])) / (y(Corner[5]) + 2)');
var gvY = (gvYleftUp + gvYrightDown) / 2
return [gvX, gvY];
}
HSLA配列[H 0-1, S 0-1, L 0-1, A 0-1]を、RGBA配列[R 0-255, G 0-255, B 0-255, A 0-255]に変換
function getRGBAfromHSLA(hslaArray){
var H = hslaArray[0] * 360;
var S = hslaArray[1] * 100;
var L = hslaArray[2] * 100;
H = H % 360;
var arr = [];
if(L <= 49){
var MAX = 2.55 * (L + L * (S / 100));
var MIN = 2.55 * (L - L * (S / 100));
}
if(L >= 50){
var MAX = 2.55 * (L + (100 - L) * (S / 100));
var MIN = 2.55 * (L - (100 -L) * (S / 100));
}
if(H < 60){
arr[0] = MAX;
arr[1] = (H / 60) * (MAX - MIN) + MIN;
arr[2] = MIN;
}
if(60 <= H && H < 120){
arr[0] = ((120 - H) / 60) * (MAX - MIN) + MIN;
arr[1] = MAX;
arr[2] = MIN;
}
if(120 <= H && H < 180){
arr[0] = MIN;
arr[1] = MAX;
arr[2] = ((H - 120) / 60) * (MAX - MIN) + MIN;
}
if(180 <= H && H < 240){
arr[0] = MIN;
arr[1] = ((240 - H) / 60) * (MAX - MIN) + MIN;
arr[2] = MAX;
}
if(240 <= H && H < 300){
arr[0] = ((H - 240) / 60) * (MAX - MIN) + MIN;
arr[1] = MIN;
arr[2] = MAX;
}
if(300 <= H && H < 360){
arr[0] = MAX;
arr[1] = MIN;
arr[2] = ((360 - H) / 60) * (MAX - MIN) + MIN;
}
arr[0] = Math.round(arr[0]);
arr[1] = Math.round(arr[1]);
arr[2] = Math.round(arr[2]);
arr[3] = hslaArray[3] * 255;
return arr;
}
ピクセル操作の際には、RGBA配列データ(全要素とも0〜255)が必要になる。個人的には、色指定はHSLAの方が考えやすいので、上記関数を作成した次第である。
HSLA配列を用いて、指定したピクセル通し番号に対応するピクセルを操作
function setPixelColorByHSLA(canvas, pixelIndex, hslaArray){
var context = canvas.getContext('2d');
var styleWidth = canvas.style.width;
styleWidth = eval( styleWidth.slice( 0, styleWidth.indexOf('px') ) );
if(!styleWidth){styleWidth = canvas.width;}
var styleHeight = canvas.style.height;
styleHeight = eval( styleHeight.slice( 0, styleHeight.indexOf('px') ) );
if(!styleHeight){styleHeight = canvas.height;}
pixelIndex = pixelIndex % ( styleWidth * styleHeight );
var pixelX = pixelIndex % styleWidth;
var pixelY = Math.floor(pixelIndex / styleWidth);
var imageData = context.getImageData(0, 0, styleWidth, styleHeight);
var pixelArray = imageData.data;
var rgbaArray = getRGBAfromHSLA(hslaArray);
var base = (pixelY * styleWidth + pixelX) * 4;
pixelArray[base + 0] = rgbaArray[0];
pixelArray[base + 1] = rgbaArray[1];
pixelArray[base + 2] = rgbaArray[2];
pixelArray[base + 3] = rgbaArray[3];
context.putImageData(imageData, 0, 0);
}
ggbApplet APIを用いて、usicanvasのピクセルを操作
var currentIndex = 0;
function drawUsiCanvasWithGgbApplet(){
var usicanvas = document.getElementsByClassName('usicanvas')[0];
var gvX = getCoordsFromPixelIndex(currentIndex)[0];
var gvY = getCoordsFromPixelIndex(currentIndex)[1];
var H = ggbApplet.getValue('Length[('+gvX+','+gvY+')]');
var S = 1;
var L = 0.5;
var A = 0.6;
var hslaArray = [H,S,L,A];
setPixelColorByHSLA(usicanvas, currentIndex, hslaArray);
currentIndex++;
}
すべてのピクセルについて、ggbApplet.getValueを実行すると、処理がとても重い。
そこで、ggbApplet.getValueを使うのは、GV1左上の座標情報と、1ピクセルの、GV1における大きさ情報を取得するときだけにしたい(グローバル変数扱い)。
その他のピクセルの、GV1における座標情報の取得や、それを用いた数式処理は、Math オブジェクトを活用するなどして、できるだけggbApplet APIを使わない方向で設計すべきであろう。