diff --git a/demo/turtle2.html b/demo/turtle2.html index 00991e7a..66ba5bb0 100644 --- a/demo/turtle2.html +++ b/demo/turtle2.html @@ -88,33 +88,6 @@

星描画2

- -
-

フラクタル描画

- -

- -
- -
diff --git a/src/plugin_turtle.mjs b/src/plugin_turtle.mjs index f29a750a..8805be84 100644 --- a/src/plugin_turtle.mjs +++ b/src/plugin_turtle.mjs @@ -1,646 +1,695 @@ -// @ts-nocheck /** * Turtle Graphics for Web browser (nadesiko3) - * plugin_turtle.js + * plugin_turtle.mts */ - -import turtleImage from './image_turtle64.mjs' -import elephantImage from './image_turtle-elephant.mjs' -import pandaImage from './image_turtle-panda.mjs' - -const PluginTurtle = { - '初期化': { - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - /* istanbul ignore if */ - if (sys._turtle) { return } - sys._turtle = { - list: [], - target: -1, - ctx: null, - canvas: null, - canvas_r: { left: 0, top: 0, width: 640, height: 400 }, - clearAll: function () { - const me = this - for (let i = 0; i < me.list.length; i++) { - const tt = me.list[i] - tt.mlist = [] // ジョブをクリア - document.body.removeChild(tt.canvas) - } - me.list = [] - if (me.canvas !== null) { - me.ctx.clearRect(0, 0, - me.canvas.width, - me.canvas.height) - } - - me.target = -1 - me.flagSetTimer = false - }, - drawTurtle: function (id) { - const tt = this.list[id] - if (!tt) { return } - const cr = this.canvas_r - // カメの位置を移動 - tt.canvas.style.left = (cr.left + tt.x - tt.cx) + 'px' - tt.canvas.style.top = (cr.top + tt.y - tt.cx) + 'px' - if (!tt.f_update) { return } - /* istanbul ignore if */ - if (!tt.flagLoaded) { return } - tt.f_update = false - tt.ctx.clearRect(0, 0, - tt.canvas.width, - tt.canvas.height) - if (!tt.f_visible) { return } - if (tt.dir !== 270) { - const rad = (tt.dir + 90) * 0.017453292519943295 - tt.ctx.save() - tt.ctx.translate(tt.cx, tt.cy) - tt.ctx.rotate(rad) - tt.ctx.translate(-tt.cx, -tt.cy) - tt.ctx.drawImage(tt.img, 0, 0) - tt.ctx.restore() - } else { tt.ctx.drawImage(tt.img, 0, 0) } - }, - getCur: function () { - if (this.list.length === 0) { throw Error('最初に『カメ作成』命令を呼び出してください。') } - - return this.list[this.target] - }, - flagSetTimer: false, - setTimer: function () { - if (this.flagSetTimer) { return } - this.flagSetTimer = true - console.log('[TURTLE] standby ...') - setTimeout(() => { - console.log('[TURTLE] Let\'s go!') - sys._turtle.play() - }, 1) - }, - line: function (tt, x1, y1, x2, y2) { - /* istanbul ignore else */ - if (tt) { if (!tt.flagDown) { return } } - - const ctx = this.ctx - if (tt.flagBegeinPath) { - ctx.lineTo(x2, y2) - } else { - ctx.beginPath() - ctx.lineWidth = tt.lineWidth - ctx.strokeStyle = tt.color - ctx.moveTo(x1, y1) - ctx.lineTo(x2, y2) - ctx.stroke() - } - }, - doMacro: function (tt, wait) { - const me = this - if (!tt.flagLoaded && wait > 0) { +import { turtleImage, elephantImage, pandaImage } from './plugin_turtle_images.mjs'; +class NakoTurtle { + constructor(sys, id) { + this.sys = sys; + this.id = id; + this.img = null; + this.canvas = null; + this.ctx = null; + this.dir = 270; // 上向き + this.cx = 32; + this.cy = 32; + this.x = 0; + this.y = 0; + this.color = 'black'; + this.lineWidth = 4; + this.flagDown = true; + this.flagBegeinPath = false; + this.f_update = true; + this.flagLoaded = false; + this.f_visible = true; + this.mlist = []; + } + clear() { + this.mlist = []; // ジョブをクリア + document.body.removeChild(this.canvas); + } + loadImage(url, callback) { + const tt = this; + this.canvas = document.createElement('canvas'); + this.ctx = tt.canvas.getContext('2d'); + this.canvas.id = this.id; + this.img = document.createElement('img'); + this.img.onload = () => { + tt.cx = tt.img.width / 2; + tt.cy = tt.img.height / 2; + tt.canvas.width = tt.img.width; + tt.canvas.height = tt.img.height; + tt.flagLoaded = true; + tt.f_update = true; + tt.canvas.style.position = 'absolute'; + document.body.appendChild(tt.canvas); + // console.log('createTurtle::this.turtles=', this) + callback(tt); + }; + this.img.onerror = () => { + console.log('カメの読み込みに失敗'); + tt.flagLoaded = true; + tt.f_visible = false; + tt.f_update = true; + callback(tt); + }; + this.img.src = url; + } +} +class NakoTurtleSystem { + static getInstance(sys) { + if (NakoTurtleSystem.instance === undefined) { + NakoTurtleSystem.instance = new NakoTurtleSystem(sys); + } + const i = NakoTurtleSystem.instance; + i.instanceCount += 1; + // console.log('@@instanceCount=', i.instanceCount) + return NakoTurtleSystem.instance; + } + constructor(sys) { + this.sys = sys; + this.turtles = []; // カメの一覧 + this.target = -1; + this.ctx = null; + this.canvas = null; + this.canvas_r = { left: 0, top: 0, width: 640, height: 400 }; + this.flagSetTimer = false; + this.instanceCount = 0; + this.timerId = null; + } + clearAll() { + // console.log('カメ全消去 turtles=', this.turtles) + for (let i = 0; i < this.turtles.length; i++) { + const tt = this.turtles[i]; + tt.clear(); + } + this.turtles = []; + if (this.canvas !== null) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + } + this.target = -1; + this.flagSetTimer = false; + } + drawTurtle(id) { + const tt = this.turtles[id]; + if (!tt) { + return; + } + const cr = this.canvas_r; + // カメの位置を移動 + tt.canvas.style.left = (cr.left + tt.x - tt.cx) + 'px'; + tt.canvas.style.top = (cr.top + tt.y - tt.cx) + 'px'; + if (!tt.f_update) { + return; + } + /* istanbul ignore if */ + if (!tt.flagLoaded) { + return; + } + tt.f_update = false; + tt.ctx.clearRect(0, 0, tt.canvas.width, tt.canvas.height); + if (!tt.f_visible) { + return; + } + if (tt.dir !== 270) { + const rad = (tt.dir + 90) * 0.017453292519943295; + tt.ctx.save(); + tt.ctx.translate(tt.cx, tt.cy); + tt.ctx.rotate(rad); + tt.ctx.translate(-tt.cx, -tt.cy); + tt.ctx.drawImage(tt.img, 0, 0); + tt.ctx.restore(); + } + else { + tt.ctx.drawImage(tt.img, 0, 0); + } + } + getCur() { + if (this.turtles.length === 0) { + throw Error('最初に『カメ作成』命令を呼び出してください。'); + } + return this.turtles[this.target]; + } + setTimer() { + if (this.flagSetTimer) { + return; + } + this.flagSetTimer = true; + console.log('[TURTLE] standby ...'); + if (this.timerId) { + clearTimeout(this.timerId); + } + this.timerId = setTimeout(() => { + console.log('[TURTLE] Let\'s go!'); + this.play(); + }, 1); + } + line(tt, x1, y1, x2, y2) { + /* istanbul ignore else */ + if (tt) { + if (!tt.flagDown) { + return; + } + } + const ctx = this.ctx; + if (tt.flagBegeinPath) { + ctx.lineTo(x2, y2); + } + else { + ctx.beginPath(); + ctx.lineWidth = tt.lineWidth; + ctx.strokeStyle = tt.color; + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + } + doMacro(tt, wait) { + const me = this; + if (!tt.flagLoaded && wait > 0) { // console.log('[TURTLE] waiting ...') - return true - } - const m = tt.mlist.shift() - const cmd = (m !== undefined) ? m[0] : '' - switch (cmd) { + return true; + } + const m = tt.mlist.shift(); + const cmd = (m !== undefined) ? m[0] : ''; + switch (cmd) { case 'xy': - // 起点を移動する - tt.x = m[1] - tt.y = m[2] - break + // 起点を移動する + tt.x = m[1]; + tt.y = m[2]; + break; case 'begin': - // 描画を明示的に開始する - this.ctx.beginPath() - this.ctx.moveTo(tt.x, tt.y) - tt.flagBegeinPath = true - break + // 描画を明示的に開始する + this.ctx.beginPath(); + this.ctx.moveTo(tt.x, tt.y); + tt.flagBegeinPath = true; + break; case 'close': - // パスを閉じる - this.ctx.closePath() - tt.flagBegeinPath = false - break + // パスを閉じる + this.ctx.closePath(); + tt.flagBegeinPath = false; + break; case 'fill': - if (tt.flagBegeinPath) { - this.ctx.closePath() - tt.flagBegeinPath = false - } - this.ctx.fill() - break + if (tt.flagBegeinPath) { + this.ctx.closePath(); + tt.flagBegeinPath = false; + } + this.ctx.fill(); + break; case 'stroke': - if (tt.flagBegeinPath) { - this.ctx.closePath() - tt.flagBegeinPath = false - } - this.ctx.stroke() - break + if (tt.flagBegeinPath) { + this.ctx.closePath(); + tt.flagBegeinPath = false; + } + this.ctx.stroke(); + break; case 'text': - this.ctx.fillText(m[1], tt.x, tt.y) - break + this.ctx.fillText(m[1], tt.x, tt.y); + break; case 'textset': - this.ctx.font = m[1] - break + this.ctx.font = m[1]; + break; case 'fillStyle': - this.ctx.fillStyle = m[1] - break + this.ctx.fillStyle = m[1]; + break; case 'mv': { - // 線を引く - me.line(tt, tt.x, tt.y, m[1], m[2]) - // カメの角度を変更 - const mvRad = Math.atan2(m[2] - tt.y, m[1] - tt.x) - tt.dir = mvRad * 57.29577951308232 - tt.f_update = true - // 実際に位置を移動 - tt.x = m[1] - tt.y = m[2] - break + // 線を引く + me.line(tt, tt.x, tt.y, m[1], m[2]); + // カメの角度を変更 + const mvRad = Math.atan2(m[2] - tt.y, m[1] - tt.x); + tt.dir = mvRad * 57.29577951308232; + tt.f_update = true; + // 実際に位置を移動 + tt.x = m[1]; + tt.y = m[2]; + break; } case 'fd': { - const fdv = m[1] * m[2] - const rad = tt.dir * 0.017453292519943295 - const x2 = tt.x + Math.cos(rad) * fdv - const y2 = tt.y + Math.sin(rad) * fdv - me.line(tt, tt.x, tt.y, x2, y2) - tt.x = x2 - tt.y = y2 - break + const fdv = m[1] * m[2]; + const rad = tt.dir * 0.017453292519943295; + const x2 = tt.x + Math.cos(rad) * fdv; + const y2 = tt.y + Math.sin(rad) * fdv; + me.line(tt, tt.x, tt.y, x2, y2); + tt.x = x2; + tt.y = y2; + break; } case 'angle': { - const angle = m[1] - tt.dir = ((angle - 90 + 360) % 360) - tt.f_update = true - break + const angle = m[1]; + tt.dir = ((angle - 90 + 360) % 360); + tt.f_update = true; + break; } case 'rotr': { - const rv = m[1] - tt.dir = (tt.dir + rv) % 360 - tt.f_update = true - break + const rv = m[1]; + tt.dir = (tt.dir + rv) % 360; + tt.f_update = true; + break; } case 'rotl': { - const lv = m[1] - tt.dir = (tt.dir - lv + 360) % 360 - tt.f_update = true - break + const lv = m[1]; + tt.dir = (tt.dir - lv + 360) % 360; + tt.f_update = true; + break; } case 'color': - tt.color = m[1] - this.ctx.strokeStyle = tt.color - break + tt.color = m[1]; + this.ctx.strokeStyle = tt.color; + break; case 'size': - tt.lineWidth = m[1] - this.ctx.lineWidth = tt.lineWidth - break + tt.lineWidth = m[1]; + this.ctx.lineWidth = tt.lineWidth; + break; case 'penOn': - tt.flagDown = m[1] - break + tt.flagDown = m[1]; + break; case 'visible': - tt.f_visible = m[1] - tt.f_update = true - break + tt.f_visible = m[1]; + tt.f_update = true; + break; case 'changeImage': - tt.flagLoaded = false - tt.img.src = m[1] - break - } - if (tt.flagLoaded) { sys._turtle.drawTurtle(tt.id) } - return (tt.mlist.length > 0) - }, - doMacroAll: function (wait) { - let hasNext = false - for (let i = 0; i < sys._turtle.list.length; i++) { - const tt = sys._turtle.list[i] - if (this.doMacro(tt, wait)) { hasNext = true } - } - return hasNext - }, - play: function () { - const me = this - const wait = sys.__v0['カメ速度'] - let hasNext = this.doMacroAll(wait) - if (wait <= 0) { - while (hasNext) { hasNext = this.doMacroAll(wait) } - } else if (hasNext) { - setTimeout(() => me.play(), wait) - return - } - console.log('[TURTLE] finished.') - me.flagSetTimer = false - }, - setupCanvas: function (sys) { - // 描画先をセットする - let canvasId = sys.__v0['カメ描画先'] - if (typeof canvasId === 'string') { - canvasId = document.getElementById(canvasId) || document.querySelector(canvasId) - sys.__v0['カメ描画先'] = canvasId - } - console.log('カメ描画先=', canvasId) - const cv = sys._turtle.canvas = canvasId - if (!cv) { - console.log('[ERROR] カメ描画先が見当たりません。' + canvasId) - throw Error('カメ描画先が見当たりません。') - } - const ctx = sys._turtle.ctx = sys._turtle.canvas.getContext('2d') - ctx.lineWidth = 4 - ctx.strokeStyle = 'black' - ctx.lineCap = 'round' - sys._turtle.resizeCanvas(sys) - }, - resizeCanvas: function (sys) { - const cv = sys._turtle.canvas - const rect = cv.getBoundingClientRect() - const rx = rect.left + window.pageXOffset - const ry = rect.top + window.pageYOffset - sys._turtle.canvas_r = { + tt.flagLoaded = false; + tt.img.src = m[1]; + break; + } + if (tt.flagLoaded) { + this.drawTurtle(tt.id); + } + return (tt.mlist.length > 0); + } + doMacroAll(wait) { + let hasNext = false; + for (let i = 0; i < this.turtles.length; i++) { + const tt = this.turtles[i]; + if (this.doMacro(tt, wait)) { + hasNext = true; + } + } + return hasNext; + } + play() { + const me = this; + const wait = this.sys.__getSysVar('カメ速度'); + let hasNext = this.doMacroAll(wait); + if (wait <= 0) { + while (hasNext) { + hasNext = this.doMacroAll(wait); + } + } + else if (hasNext) { + if (this.timerId) { + clearTimeout(this.timerId); + } + this.timerId = setTimeout(() => me.play(), wait); + return; + } + console.log('[TURTLE] finished.'); + me.flagSetTimer = false; + } + setupCanvas() { + // 描画先をセットする + let canvasId = this.sys.__getSysVar('カメ描画先'); + if (typeof canvasId === 'string') { + canvasId = document.getElementById(canvasId) || document.querySelector(canvasId); + this.sys.__setSysVar('カメ描画先', canvasId); + } + console.log('カメ描画先=', canvasId); + const cv = this.canvas = canvasId; + if (!cv) { + console.log('[ERROR] カメ描画先が見当たりません。' + canvasId); + throw Error('カメ描画先が見当たりません。'); + } + const ctx = this.ctx = cv.getContext('2d'); + ctx.lineWidth = 4; + ctx.strokeStyle = 'black'; + ctx.lineCap = 'round'; + this.resizeCanvas(); + } + resizeCanvas() { + const cv = this.canvas; + const rect = cv.getBoundingClientRect(); + const rx = rect.left + window.scrollX; + const ry = rect.top + window.scrollY; + this.canvas_r = { 'left': rx, 'top': ry, width: rect.width, height: rect.height - } - }, - createTurtle: function (imageUrl, sys) { - // キャンバス情報は毎回参照する (#734) - sys._turtle.setupCanvas(sys) - // const cv = sys._turtle.canvas - // カメの情報を sys._turtle リストに追加 - const id = sys._turtle.list.length - const tt = { - id: id, - img: null, - canvas: null, - ctx: null, - dir: 270, // 上向き - cx: 32, - cy: 32, - x: 0, - y: 0, - color: 'black', - lineWidth: 4, - flagDown: true, - flagBegeinPath: false, - f_update: true, - flagLoaded: false, - f_visible: true, - mlist: [] - } - sys._turtle.list.push(tt) - sys._turtle.target = id - // 画像を読み込む - tt.img = document.createElement('img') - tt.canvas = document.createElement('canvas') - tt.ctx = tt.canvas.getContext('2d') - tt.canvas.id = id - tt.img.onload = () => { - tt.cx = tt.img.width / 2 - tt.cy = tt.img.height / 2 - tt.canvas.width = tt.img.width - tt.canvas.height = tt.img.height - tt.flagLoaded = true - tt.f_update = true - sys._turtle.drawTurtle(tt.id) - console.log('turtle.onload') - } - tt.img.onerror = () => { - console.log('カメの読み込みに失敗') - tt.flagLoaded = true - tt.f_visible = false - tt.f_update = true - sys._turtle.drawTurtle(tt.id) - } - tt.img.src = imageUrl - tt.canvas.style.position = 'absolute' - document.body.appendChild(tt.canvas) - // デフォルト位置の設定 - tt.x = sys._turtle.canvas_r.width / 2 - tt.y = sys._turtle.canvas_r.height / 2 - return id - } - } - } - }, - - '!クリア': { - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - sys._turtle.clearAll() - } - }, - - // @タートルグラフィックス・カメ描画 - 'カメ作成': { // @タートルグラフィックスを開始してカメのIDを返す // @かめさくせい - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const imageUrl = sys.__v0['カメ画像URL'] - return sys._turtle.createTurtle(imageUrl, sys) + }; } - }, - 'ゾウ作成': { // @ゾウの画像でタートルグラフィックスを開始してIDを返す // @ぞうさくせい - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const imageUrl = elephantImage - return sys._turtle.createTurtle(imageUrl, sys) + createTurtle(imageUrl) { + const self = this; + // キャンバス情報は毎回参照する (#734) + this.setupCanvas(); + // カメの情報をリストに追加 + const id = this.turtles.length; + const tt = new NakoTurtle(this.sys, id); + this.turtles.push(tt); + this.target = id; + // 画像を読み込む + tt.loadImage(imageUrl, (tt) => { + self.drawTurtle(tt.id); + console.log(`tutrle.onload(id=${tt.id})`); + }); + // デフォルト位置(中央)の設定 + tt.x = self.canvas_r.width / 2; + tt.y = self.canvas_r.height / 2; + return id; } - }, - 'パンダ作成': { // @パンダの画像でタートルグラフィックスを開始してIDを返す // @ぱんださくせい - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const imageUrl = pandaImage - return sys._turtle.createTurtle(imageUrl, sys) - } - }, - 'カメ操作対象設定': { // @IDを指定して操作対象となるカメを変更する // @かめそうさたいしょうせってい - type: 'func', - josi: [['に', 'へ', 'の']], - pure: true, - fn: function (id, sys) { - sys._turtle.target = id +} +const PluginTurtle = { + '初期化': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const turtleSystem = NakoTurtleSystem.getInstance(sys); + sys.tags._turtle = turtleSystem; + } }, - return_none: true - }, - 'カメ描画先': { type: 'var', value: 'turtle_cv' }, // @かめびょうがさき - 'カメ画像URL': { type: 'var', value: turtleImage }, // @かめがぞうURL - 'カメ画像変更': { // @カメの画像をURLに変更する // @かめがぞうへんこう - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (url, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['changeImage', url]) - sys._turtle.setTimer() + '!クリア': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + // console.log('tutle::!クリア') + sys.tags._turtle.clearAll(); + } }, - return_none: true - }, - 'カメ速度': { type: 'const', value: 100 }, // @かめそくど - 'カメ速度設定': { // @カメの動作速度vに設定(大きいほど遅い) // @かめそくどせってい - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (v, sys) { - sys.__setSysVar('カメ速度', v) - } - }, - 'カメ移動': { // @カメの位置を[x,y]へ移動する // @かめいどう - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (xy, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['mv', xy[0], xy[1]]) - sys._turtle.setTimer() + // @タートルグラフィックス・カメ描画 + 'カメ作成': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const imageUrl = sys.__getSysVar('カメ画像URL'); + return sys.tags._turtle.createTurtle(imageUrl); + } }, - return_none: true - }, - 'カメ起点移動': { // @カメの描画起点位置を[x,y]へ移動する // @かめきてんいどう - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (xy, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['xy', xy[0], xy[1]]) - sys._turtle.setTimer() + 'ゾウ作成': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const imageUrl = elephantImage; + return sys.tags._turtle.createTurtle(imageUrl); + } }, - return_none: true - }, - 'カメ進': { // @カメの位置をVだけ進める // @かめすすむ - type: 'func', - josi: [['だけ']], - pure: true, - fn: function (v, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['fd', v, 1]) - sys._turtle.setTimer() + 'パンダ作成': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const imageUrl = pandaImage; + return sys.tags._turtle.createTurtle(imageUrl); + } }, - return_none: true - }, - 'カメ戻': { // @カメの位置をVだけ戻す // @かめもどる - type: 'func', - josi: [['だけ']], - pure: true, - fn: function (v, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['fd', v, -1]) - sys._turtle.setTimer() + 'カメ操作対象設定': { + type: 'func', + josi: [['に', 'へ', 'の']], + pure: true, + fn: function (id, sys) { + sys.tags._turtle.target = id; + }, + return_none: true }, - return_none: true - }, - 'カメ角度設定': { // @カメの向きをDEGに設定する // @かめかくどせってい - type: 'func', - josi: [['に', 'へ', 'の']], - pure: true, - fn: function (v, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['angle', parseFloat(v)]) - sys._turtle.setTimer() + 'カメ描画先': { type: 'var', value: '#turtle_cv' }, // @かめびょうがさき + 'カメ画像URL': { type: 'var', value: turtleImage }, // @かめがぞうURL + 'カメ画像変更': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (url, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['changeImage', url]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメ右回転': { // @カメの向きをDEGだけ右に向ける // @かめみぎかいてん - type: 'func', - josi: [['だけ']], - pure: true, - fn: function (v, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['rotr', v]) - sys._turtle.setTimer() + 'カメ速度': { type: 'const', value: 100 }, // @かめそくど + 'カメ速度設定': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (v, sys) { + sys.__setSysVar('カメ速度', v); + } }, - return_none: true - }, - 'カメ左回転': { // @カメの向きをDEGだけ左に向ける // @かめひだりかいてん - type: 'func', - josi: [['だけ']], - pure: true, - fn: function (v, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['rotl', v]) - sys._turtle.setTimer() + 'カメ移動': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (xy, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['mv', xy[0], xy[1]]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメペン色設定': { // @カメのペン描画色をCに設定する // @かめぺんいろせってい - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (c, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['color', c]) - sys._turtle.setTimer() + 'カメ起点移動': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (xy, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['xy', xy[0], xy[1]]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメペンサイズ設定': { // @カメペンのサイズをWに設定する // @かめぺんさいずせってい - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (w, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['size', w]) - sys._turtle.setTimer() - } - }, - 'カメペン設定': { // @カメペンを使うかどうかをV(オン/オフ)に設定する // @かめぺんせってい - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (w, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['penOn', w]) - sys._turtle.setTimer() - } - }, - 'カメパス開始': { // @カメで明示的にパスの描画を開始する // @かめぱすかいし - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['begin']) - sys._turtle.setTimer() - } - }, - 'カメパス閉': { // @カメでパスを明示的に閉じる(省略可能) // @かめぱすとじる - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['close']) - sys._turtle.setTimer() - } - }, - 'カメパス線引': { // @カメでパスを閉じて、カメペン色設定で指定した色で枠線を引く // @かめぱすせんひく - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['stroke']) - sys._turtle.setTimer() - } - }, - 'カメパス塗': { // @カメでパスを閉じて、カメ塗り色設定で指定した色で塗りつぶす // @かめぱすぬる - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['fill']) - sys._turtle.setTimer() - } - }, - 'カメ文字描画': { // @カメの位置に文字Sを描画 // @かめもじびょうが - type: 'func', - josi: [['を', 'と', 'の']], - pure: true, - fn: function (s, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['text', s]) - sys._turtle.setTimer() - } - }, - 'カメ文字設定': { // @カメ文字描画で描画するテキストサイズやフォント(48px serif)などを設定 // @かめもじせってい - type: 'func', - josi: [['に', 'へ', 'で']], - pure: true, - fn: function (s, sys) { - s = '' + s // 文字列に - if (s.match(/^\d+$/)) { - s = s + 'px serif' - } else if (s.match(/^\d+(px|em)$/)) { - s = s + ' serif' - } - const tt = sys._turtle.getCur() - tt.mlist.push(['textset', s]) - sys._turtle.setTimer() - } - }, - 'カメ塗色設定': { // @カメパスの塗り色をCに設定する // @かめぬりいろせってい - type: 'func', - josi: [['に', 'へ']], - pure: true, - fn: function (c, sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['fillStyle', c]) - sys._turtle.setTimer() + 'カメ進': { + type: 'func', + josi: [['だけ']], + pure: true, + fn: function (v, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['fd', v, 1]); + sys.tags._turtle.setTimer(); + }, + return_none: true + }, + 'カメ戻': { + type: 'func', + josi: [['だけ']], + pure: true, + fn: function (v, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['fd', v, -1]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメ全消去': { // @表示しているカメと描画内容を全部消去する // @かめぜんしょうきょ - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - sys._turtle.clearAll() + 'カメ角度設定': { + type: 'func', + josi: [['に', 'へ', 'の']], + pure: true, + fn: function (v, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['angle', v]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメコマンド実行': { // @カメにコマンドSを実行する。コマンドは改行か「;」で区切る。コマンドと引数は「=」で区切り引数はかカンマで区切る // @かめこまんどじっこう - type: 'func', - josi: [['の', 'を']], - pure: true, - fn: function (cmd, sys) { - const tt = sys._turtle.getCur() - const a = cmd.split(/(\n|;)/) - for (let i = 0; i < a.length; i++) { - let c = a[i] - c = c.replace(/^([a-zA-Z_]+)\s*(\d+)/, '$1,$2') - c = c.replace(/^([a-zA-Z_]+)\s*=/, '$1,') - const ca = c.split(/\s*,\s*/) - tt.mlist.push(ca) - } - sys._turtle.setTimer() + 'カメ右回転': { + type: 'func', + josi: [['だけ']], + pure: true, + fn: function (v, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['rotr', v]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメ非表示': { // @カメの画像を非表示にする。描画に影響しない。 // @かめひひょうじ - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['visible', false]) - sys._turtle.setTimer() + 'カメ左回転': { + type: 'func', + josi: [['だけ']], + pure: true, + fn: function (v, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['rotl', v]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメ表示': { // @非表示にしたカメを表示する。 // @かめひょうじ - type: 'func', - josi: [], - pure: true, - fn: function (sys) { - const tt = sys._turtle.getCur() - tt.mlist.push(['visible', true]) - sys._turtle.setTimer() + 'カメペン色設定': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (c, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['color', c]); + sys.tags._turtle.setTimer(); + }, + return_none: true }, - return_none: true - }, - 'カメクリック時': { // @ 操作対象のカメをクリックした時のイベントを設定する // @かめくりっくしたとき - type: 'func', - josi: [['を']], - pure: false, - fn: function (func, sys) { - func = sys.__findVar(func, null) // 文字列指定なら関数に変換 - const tid = sys._turtle.target - const tt = sys._turtle.list[tid] - tt.canvas.onclick = (e) => { - sys.__v0['対象'] = e.target - return func(e, sys) - } + 'カメペンサイズ設定': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (w, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['size', w]); + sys.tags._turtle.setTimer(); + } }, - return_none: true - } -} - + 'カメペン設定': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (w, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['penOn', w]); + sys.tags._turtle.setTimer(); + } + }, + 'カメパス開始': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['begin']); + sys.tags._turtle.setTimer(); + } + }, + 'カメパス閉': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['close']); + sys.tags._turtle.setTimer(); + } + }, + 'カメパス線引': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['stroke']); + sys.tags._turtle.setTimer(); + } + }, + 'カメパス塗': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['fill']); + sys.tags._turtle.setTimer(); + } + }, + 'カメ文字描画': { + type: 'func', + josi: [['を', 'と', 'の']], + pure: true, + fn: function (s, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['text', s]); + sys.tags._turtle.setTimer(); + } + }, + 'カメ文字設定': { + type: 'func', + josi: [['に', 'へ', 'で']], + pure: true, + fn: function (s, sys) { + s = '' + s; // 文字列に + if (s.match(/^\d+$/)) { + s = s + 'px serif'; + } + else if (s.match(/^\d+(px|em)$/)) { + s = s + ' serif'; + } + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['textset', s]); + sys.tags._turtle.setTimer(); + } + }, + 'カメ塗色設定': { + type: 'func', + josi: [['に', 'へ']], + pure: true, + fn: function (c, sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['fillStyle', c]); + sys.tags._turtle.setTimer(); + }, + return_none: true + }, + 'カメ全消去': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + sys.tags._turtle.clearAll(); + }, + return_none: true + }, + 'カメコマンド実行': { + type: 'func', + josi: [['の', 'を']], + pure: true, + fn: function (cmd, sys) { + const tt = sys.tags._turtle.getCur(); + const a = cmd.split(/(\n|;)/); + for (let i = 0; i < a.length; i++) { + let c = a[i]; + c = c.replace(/^([a-zA-Z_]+)\s*(\d+)/, '$1,$2'); + c = c.replace(/^([a-zA-Z_]+)\s*=/, '$1,'); + const ca = c.split(/\s*,\s*/); + tt.mlist.push(ca); + } + sys.tags._turtle.setTimer(); + }, + return_none: true + }, + 'カメ非表示': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['visible', false]); + sys.tags._turtle.setTimer(); + }, + return_none: true + }, + 'カメ表示': { + type: 'func', + josi: [], + pure: true, + fn: function (sys) { + const tt = sys.tags._turtle.getCur(); + tt.mlist.push(['visible', true]); + sys.tags._turtle.setTimer(); + }, + return_none: true + }, + 'カメクリック時': { + type: 'func', + josi: [['を']], + pure: false, + fn: function (func, sys) { + func = sys.__findVar(func, null); // 文字列指定なら関数に変換 + const tid = sys.tags._turtle.target; + const tt = sys.tags._turtle.list[tid]; + tt.canvas.onclick = (e) => { + sys.__setSysVar('対象', e.target); + return func(e, sys); + }; + }, + return_none: true + } +}; // module.exports = PluginTurtle -export default PluginTurtle - +export default PluginTurtle; // scriptタグで取り込んだ時、自動で登録する -/* istanbul ignore else */ -if (typeof (navigator) === 'object' && typeof (navigator.nako3)) { navigator.nako3.addPluginObject('PluginTurtle', PluginTurtle) } +// @ts-ignore TS2339 +if (typeof (navigator) === 'object' && typeof (navigator.nako3)) { + navigator.nako3.addPluginObject('PluginTurtle', PluginTurtle); +} diff --git a/src/plugin_turtle_images.mts b/src/plugin_turtle_images.mts new file mode 100644 index 00000000..bf25a0f4 --- /dev/null +++ b/src/plugin_turtle_images.mts @@ -0,0 +1,12 @@ +/** + * images + */ +export const turtleImage = 'data:image/png;base64,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' +export const elephantImage = 'data:image/png;base64,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' +export const pandaImage = 'data:image/png;base64,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' + + + + + +