├── .gitignore ├── image └── description.gif ├── css └── theme.css ├── LICENSE.txt ├── js ├── lib │ ├── paperjs-v0.12.17 │ │ └── node │ │ │ ├── xml.js │ │ │ ├── self.js │ │ │ ├── canvas.js │ │ │ └── extend.js │ ├── matter-attractors.js │ ├── matter-wrap.js │ ├── decomp.js │ └── jquery-3.7.1.slim.min.js └── simple-svg-physics-runner.js ├── index.html ├── README_ja.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | _backup/ 2 | 3 | .DS_Store 4 | 5 | -------------------------------------------------------------------------------- /image/description.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shspage/simple-svg-physics-runner/HEAD/image/description.gif -------------------------------------------------------------------------------- /css/theme.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | position:relative; 5 | height: 100%; 6 | -webkit-user-select: none; 7 | } 8 | 9 | canvas 10 | { 11 | background-color:#ccc; 12 | display:block; 13 | } 14 | canvas[resize] { 15 | width: 100%; 16 | height: 96%; 17 | } 18 | 19 | #status_en, #status_ja{ 20 | font-size:80%; 21 | } 22 | 23 | #div_canvas{ 24 | top: 0px; 25 | position: absolute; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | #div_help{ 30 | top: 0px; 31 | left: 20px; 32 | position: absolute; 33 | color:#888; 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Hiroyuki Sato 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the Software 9 | is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /js/lib/paperjs-v0.12.17/node/xml.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 3 | * http://paperjs.org/ 4 | * 5 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 6 | * http://juerglehni.com/ & https://puckey.studio/ 7 | * 8 | * Distributed under the MIT license. See LICENSE file for details. 9 | * 10 | * All rights reserved. 11 | */ 12 | 13 | module.exports = function(self) { 14 | // Define XMLSerializer shim, to emulate browser behavior. 15 | // Effort to bring XMLSerializer to jsdom: 16 | // https://github.com/tmpvar/jsdom/issues/1368 17 | self.XMLSerializer = function XMLSerializer() { 18 | }; 19 | 20 | self.XMLSerializer.prototype = { 21 | serializeToString: function(node) { 22 | if (!node) 23 | return ''; 24 | // Fix a jsdom issue where all SVG tagNames are lowercased: 25 | // https://github.com/tmpvar/jsdom/issues/620 26 | var text = node.outerHTML, 27 | tagNames = ['linearGradient', 'radialGradient', 'clipPath', 28 | 'textPath']; 29 | for (var i = 0, l = tagNames.length; i < l; i++) { 30 | var tagName = tagNames[i]; 31 | text = text.replace( 32 | new RegExp('(<|', { 41 | // Use the current working directory as the document's origin, so 42 | // requests to local files work correctly with CORS. 43 | url: 'file://' + process.cwd() + '/', 44 | resources: 'usable' 45 | }); 46 | self = document.window; 47 | require('./canvas.js')(self, requireName); 48 | require('./xml.js')(self); 49 | } else { 50 | self = { 51 | navigator: { 52 | userAgent: 'Node.js (' + process.platform + '; U; rv:' + 53 | process.version + ')' 54 | } 55 | }; 56 | } 57 | 58 | module.exports = self; 59 | -------------------------------------------------------------------------------- /js/lib/paperjs-v0.12.17/node/canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 3 | * http://paperjs.org/ 4 | * 5 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 6 | * http://juerglehni.com/ & https://puckey.studio/ 7 | * 8 | * Distributed under the MIT license. See LICENSE file for details. 9 | * 10 | * All rights reserved. 11 | */ 12 | 13 | // Add some useful extensions to HTMLCanvasElement: 14 | // - HTMLCanvasElement#type, so we can switch to a PDF canvas 15 | // - Various Node-Canvas methods, routed through from HTMLCanvasElement: 16 | // toBuffer, pngStream, createPNGStream, jpegStream, createJPEGStream 17 | 18 | module.exports = function(self, requireName) { 19 | var Canvas; 20 | try { 21 | Canvas = require('canvas').Canvas; 22 | } catch(error) { 23 | // Remove `self.window`, so we still have the global `self` reference, 24 | // but no `window` object: 25 | // - On the browser, this corresponds to a worker context. 26 | // - On Node.js, it basically means the canvas is missing or not working 27 | // which can be treated the same way. 28 | delete self.window; 29 | // Check the required module's name to see if it contains canvas, and 30 | // only complain about its lack if the module requires it. 31 | if (/\bcanvas\b/.test(requireName)) { 32 | throw new Error('Unable to load canvas module.'); 33 | } 34 | return; 35 | } 36 | 37 | var HTMLCanvasElement = self.HTMLCanvasElement, 38 | idlUtils = require('jsdom/lib/jsdom/living/generated/utils'); 39 | 40 | // Add fake HTMLCanvasElement#type property: 41 | Object.defineProperty(HTMLCanvasElement.prototype, 'type', { 42 | get: function() { 43 | var canvas = idlUtils.implForWrapper(this)._canvas; 44 | return canvas && canvas.type || 'image'; 45 | }, 46 | 47 | set: function(type) { 48 | // Allow replacement of internal node-canvas, so we can switch to a 49 | // PDF canvas. 50 | var impl = idlUtils.implForWrapper(this), 51 | size = impl._canvas || impl; 52 | impl._canvas = new Canvas(size.width, size.height, type); 53 | impl._context = null; 54 | } 55 | }); 56 | 57 | // Extend HTMLCanvasElement with useful methods from the underlying Canvas: 58 | var methods = ['toBuffer', 'pngStream', 'createPNGStream', 'jpegStream', 59 | 'createJPEGStream']; 60 | methods.forEach(function(key) { 61 | HTMLCanvasElement.prototype[key] = function() { 62 | var canvas = idlUtils.implForWrapper(this)._canvas; 63 | return canvas[key].apply(canvas, arguments); 64 | }; 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 |
19 | [ Help ] show/hide
20 | 
21 |
22 |
23 | v : paste SVG
24 | s : export SVG
25 | p : pause / play
26 | w : toggle wireframe
27 | g : toggle gravity
28 | a : toggle airResistance
29 | 
30 | 57 | 77 |
78 | ----
79 | created by @shspage
80 | repository of this project is on GitHub
81 | 
82 |
83 | 88 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | # simple-svg-physics-runner 2 | 3 | ### 概要 4 | * ファイルまたはクリップボードを介してSVGデータを読み込みます。 5 | * 読み込んだ形状は物理法則に従って動きます。 6 | * 表示内容はSVGデータとして出力できます。 7 | 8 | ![description](https://github.com/shspage/simple-svg-physics-runner/blob/master/image/description.gif) 9 | 10 | * バージョン 1.1.2b 以降、重力の初期値は 0 になりました。通常の重力にする場合は "g" を押してください。 11 | 12 | ### 使い方 13 | * "v" を押すと、クリップボードのSVGコードを読み込みます。(Chrome 66以降) 14 | 15 | * ドラッグ&ドロップしたSVGファイルを読み込むこともできます。 16 | 17 | * 読み込めるのはクローズパスのみです。読み込み後、塗り色が**黒**のものは位置が固定されます。それ以外のものは物理法則に従って動きます。ドラッグで動かすこともできます。 18 | 19 | * "p" を押すと、ポーズ/再生 を切り替えます。 20 | 21 | * "s" を押すと、SVGファイルを出力します。 22 | 23 | * "w" を押すと、ワイヤーフレーム表示を切り替えます。 24 | 25 | * "g" を押すと、無重力状態を切り替えます。 26 | 27 | * "a" を押すと、空気抵抗(通常/重い)を切り替えます。 28 | 29 | * w,g,a は、読み込み前に切り替えておくことができます。 30 | 31 | ---- 32 | ### 進んだ使い方(Illustrator) 33 | 34 | #### レイヤー名による制御 35 | * レイヤー名に「wrap」を含む場合、画面外に出たオブジェクトは画面の反対側から入ってきます。 36 | (重力を0に設定してからの使用をおすすめします。) 37 | 38 | #### グループ名による制御 39 | * グループ名が「chain」で始まる場合、グループ内のオブジェクトは垂直方向に連結され、上部で固定されます。 40 | video - https://twitter.com/shspage/status/1757720353638674790 41 | * 同様に「bridge」で始まる場合は左右方向に連結され、両橋が固定されます。 42 | * 同様に「loop」で始まる場合はループ状に連結されます。(それぞれの最も近い・遠いオブジェクトと連結します。) 43 | * 「chain」で始まるグループ名が「 as is」(asの前に空白が必要)を含む場合、元のオブジェクトの位置を維持して連結します。 44 | 45 | #### オブジェクト名による制御 46 | * 名前が「hangN」(Nは自然数) を含む場合、上から吊り下げる線を生成します。Nが線の長さになります。加えて「offsetN」(N:-0.5〜0.5) 47 | を含む場合、線につなぐ位置を縦幅に対する比率でずらします。加えて「#N」(Nは3桁または6桁の16進数)を含む場合、線の色になります。「hidden」を含む場合、線は描画されません。これらはグループやレイヤーの名前に入っていても機能します。 48 | 49 | ### 使ってみる 50 | https://shspage.github.io/simple-svg-physics-runner/ 51 | 52 | ### 補足: 53 | * クリップボードからの読み込みはhttpプロトコルでは動作しません(httpsかfileで動作) 54 | * SVGデータ中の凸でない多角形は自動的に凸多角形の集合体になります。SVG保存時には元の形状が出力されます。 55 | 凸多角形への分割は、複雑な図形ではうまくいかない場合があります。 56 | * 【重要】Illustratorでは、SVGコード出力・生成時に、元のパスが自動的に変更されることがあります。 57 | たとえば **「線の位置」が「中央」でない線** は、線色を塗り色としてアウトライン化された複合パスになります。 58 | * 積み重なった物体などをSVG出力すると、物体の端がお互いに食い込んでいる箇所に気が付くかもしれません。 59 | コンピュータの処理能力に余裕があれば、simple-svg-physics-runner.js 冒頭の ENGINE_POSITION_ITERATIONS を増やすことで、これを緩和できると思われます。 60 | (精度向上のためにこの値を調整することが適切なのかは、私もまだよくわかりませんが。 61 | いずれにしても、物理エンジンにとってある程度の誤差は処理の安定性のために必要なものなのかもしれません。) 62 | 上記のスクリプト冒頭部には、他にも変更可能な定数がいくつかあります。しかし全ての属性を網羅しているわけではありません。 63 | 詳細は Matter.js のドキュメントを参照してください。 64 | 65 | * IllustratorのCMYKモードでの黒(K100)は、生成されるSVGコードでは#000になりません。つまり位置を固定する対象になりません。 66 | このため、RGBがぞれぞれ (35,24,21) 以下の場合は黒と見なして#000に変換し、エクスポート時も#000にしています。 67 | この色変換を行いたくない場合、simple-svg-physics-runner.js で AS_BLACK_THRESHOLD_RGB を[0,0,0] に設定してください。 68 | 69 | * 複合パスは「穴」がない図形として描画されます。穴に見えるのは背景色で塗られた図形です。このため穴の内部に他の図形を置くことはできません。 70 | 71 | 72 | ### 変更履歴 73 | #### v.1.2.0b2 74 | * "hang" で生成された線をエクスポートされるよう修正。 75 | 76 | #### v.1.2.0b1 77 | * オブジェクト名中の "hang" 等による制御を追加。 78 | * SVG要素の属性に data-name がある場合、idに代えて使用するよう変更。 79 | 80 | #### v.1.2.0b 81 | * 曲線を適宜折れ線に変換する処理を追加。 82 | * 物理エンジンで扱うにあたり多角形の分割や折れ線化をした場合でも、エクスポート時は元のSVGの形状を保存するように変更。 83 | 84 | #### v.1.1.2b1 85 | * 重力が 0 の場合の文字表示を赤色に変更。通常の重力の値を 1 に変更。 86 | * bodyにrestitution(反発)の値を設定。 87 | * "chain", "bridge" の最後の constraint が固い問題に暫定的に対応。 88 | 89 | #### v.1.1.2b 90 | * 重力の初期値を 0 に変更。通常の重力の値を 0.8 に変更。 91 | * "wrap"機能を追加。(matter-wrapのバージョン整合性が一致しませんが動作しているようです。) 92 | * 複合パスの描画に対応。(note の注意事項もご覧ください。) 93 | 94 | #### v.1.1.1 95 | * Illustrator: グループ名が"loop"で始まる場合に、中のオブジェクトをループ状に連結する機能を追加。 96 | * Illustrator: グループ名が"chain"で始まり、" as is"を含む場合に、中のオブジェクトを元の位置を維持して連結する機能を追加。 97 | 98 | #### v.1.1.0 99 | * 必須ライブラリを更新しました。 100 | * canvasからスクロールバーを取り除きました。 101 | * Illustrator: グループ名が"bridge"で始まる場合に、中のオブジェクトを左右方向に連結する機能を追加。 102 | 103 | #### v.1.0.2 104 | * RGB値が全て設定値以下の場合黒と見なす処理を追加。(AS_BLACK_THRESHOLD_RGB) 105 | * Illustrator: グループ名が"chain"で始まる場合に、中のオブジェクトを上下方向に連結する機能を追加。 106 | 107 | #### v.1.0.1 108 | * 幅と高さの差、および幅を直径として算出した周長・面積と実際の値との差が設定値未満であれば、パスを円と見なすようにしました。 109 | (Inkscapeで作成したSVGへの対応用。判別方法はまだ検討が必要かもしれません。) 110 | * 読み込んだ図形の左上をキャンバスの左上に合わせるようにしました。 111 | 112 | ### TODO 113 | * FireFoxでのクリップボードからの読み込み 114 | * 曲線を自動的に折れ線化する 115 | 116 | ### License 117 | * Copyright(c) 2018 Hiroyuki Sato 118 | https://github.com/shspage/simple-svg-physics-runner 119 | This software is distributed under the MIT License. 120 | See the LICENSE.txt for details. 121 | 122 | This software uses the following libraries that may have licenses 123 | differing from that of the software itself. You can find the 124 | libraries and their respective licenses below. 125 | 126 | #### required libraries (including in this repo) 127 | * jQuery (v3.7.1) 128 | License MIT 129 | (c) OpenJS Foundation and other contributors | jquery.org/license 130 | 131 | * matter-js (0.19.0) http://brm.io/matter-js/ 132 | License MIT 133 | Copyright (c) Liam Brummitt and contributors. 134 | 135 | * Paper.js (v0.12.17) http://paperjs.org/ 136 | License MIT 137 | Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 138 | http://juerglehni.com/ & https://puckey.studio/ 139 | 140 | * decomp.js (https://github.com/schteppe/poly-decomp.js) 141 | License MIT 142 | Copyright (c) 2013 Stefan Hedman 143 | -------------------------------------------------------------------------------- /js/lib/paperjs-v0.12.17/node/extend.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. 3 | * http://paperjs.org/ 4 | * 5 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 6 | * http://juerglehni.com/ & https://puckey.studio/ 7 | * 8 | * Distributed under the MIT license. See LICENSE file for details. 9 | * 10 | * All rights reserved. 11 | */ 12 | 13 | var fs = require('fs'), 14 | path = require('path'); 15 | 16 | module.exports = function(paper) { 17 | if (paper.PaperScript) { 18 | var sourceMapSupport = 'require("source-map-support").install(paper.PaperScript.sourceMapSupport);\n', 19 | sourceMaps = {}; 20 | 21 | paper.PaperScript.sourceMapSupport = { 22 | retrieveSourceMap: function(source) { 23 | var map = sourceMaps[source]; 24 | return map ? { url: source, map: map } : null; 25 | } 26 | }; 27 | 28 | // Register the .pjs extension for automatic compilation as PaperScript 29 | require.extensions['.pjs'] = function(module, filename) { 30 | // Requiring a PaperScript on Node.js returns an initialize method which 31 | // needs to receive a Canvas object when called and returns the 32 | // PaperScope. 33 | module.exports = function(canvas) { 34 | var source = fs.readFileSync(filename, 'utf8'), 35 | code = sourceMapSupport + source, 36 | compiled = paper.PaperScript.compile(code, { 37 | url: filename, 38 | source: source, 39 | sourceMaps: true, 40 | offset: -1 // remove sourceMapSupport... 41 | }), 42 | scope = new paper.PaperScope(); 43 | // Keep track of sourceMaps so retrieveSourceMap() can link them up 44 | scope.setup(canvas); 45 | scope.__filename = filename; 46 | scope.__dirname = path.dirname(filename); 47 | // Expose core methods and values 48 | scope.require = require; 49 | scope.console = console; 50 | sourceMaps[filename] = compiled.map; 51 | paper.PaperScript.execute(compiled, scope); 52 | return scope; 53 | }; 54 | }; 55 | } 56 | 57 | paper.PaperScope.inject({ 58 | createCanvas: function(width, height, type) { 59 | // Do not use CanvasProvider.getCanvas(), since we may be changing 60 | // the underlying node-canvas when requesting PDF support, and don't 61 | // want to release it after back into the pool. 62 | var canvas = paper.document.createElement('canvas'); 63 | canvas.width = width; 64 | canvas.height = height; 65 | canvas.type = type; 66 | return canvas; 67 | }, 68 | 69 | /** 70 | * @deprecated, use use {@link #createCanvas(width, height)} instead. 71 | */ 72 | Canvas: '#createCanvas' 73 | }); 74 | 75 | // Override requestAnimationFrame() to avoid setInterval() timers. 76 | // NOTE: In Node.js, we only support manual updating for now, but 77 | // View#exportFrames() below offers a way to emulate animations by exporting 78 | // them frame by frame at the given frame-rate. 79 | paper.DomEvent.requestAnimationFrame = function(callback) { 80 | }; 81 | 82 | // Node.js based image exporting code. 83 | paper.CanvasView.inject({ 84 | // DOCS: CanvasView#exportFrames(options); 85 | exportFrames: function(options) { 86 | options = paper.Base.set({ 87 | fps: 30, 88 | prefix: 'frame-', 89 | amount: 1, 90 | format: 'png' // Supported: 'png' or 'jpeg' 91 | }, options); 92 | if (!options.directory) 93 | throw new Error('Missing options.directory'); 94 | if (options.format && !/^(jpeg|png)$/.test(options.format)) 95 | throw new Error('Unsupported format. Use "png" or "jpeg"'); 96 | var view = this, 97 | count = 0, 98 | frameDuration = 1 / options.fps, 99 | startTime = Date.now(), 100 | lastTime = startTime, 101 | padding = options.padding || ((options.amount - 1) + '').length, 102 | paddedStr = Array(padding + 1).join('0'); 103 | 104 | // Start exporting frames by exporting the first frame: 105 | exportFrame(options); 106 | 107 | function exportFrame() { 108 | // Convert to a Base object, for #toString() 109 | view.emit('frame', new paper.Base({ 110 | delta: frameDuration, 111 | time: frameDuration * count, 112 | count: count 113 | })); 114 | var file = path.join(options.directory, 115 | options.prefix + (paddedStr + count).slice(-padding) 116 | + '.' + options.format); 117 | var out = view.exportImage(file, function() { 118 | // Once the file has been closed, export the next fame: 119 | var then = Date.now(); 120 | if (options.onProgress) { 121 | options.onProgress({ 122 | count: count, 123 | amount: options.amount, 124 | percentage: Math.round((count + 1) / options.amount 125 | * 10000) / 100, 126 | time: then - startTime, 127 | delta: then - lastTime 128 | }); 129 | } 130 | lastTime = then; 131 | if (++count < options.amount) { 132 | exportFrame(); 133 | } else { 134 | // Call onComplete handler when finished: 135 | if (options.onComplete) { 136 | options.onComplete(); 137 | } 138 | } 139 | }); 140 | } 141 | }, 142 | 143 | // DOCS: CanvasView#exportImage(path, callback); 144 | exportImage: function(path, callback) { 145 | this.update(); 146 | var out = fs.createWriteStream(path), 147 | format = /\.jp(e?)g$/.test(path) ? 'jpeg' : 'png', 148 | stream = this._element[format + 'Stream'](); 149 | stream.pipe(out); 150 | if (callback) { 151 | out.on('close', callback); 152 | } 153 | return out; 154 | } 155 | }); 156 | }; 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple-svg-physics-runner 2 | 3 | [Readme in Japanese](https://github.com/shspage/simple-svg-physics-runner/blob/master/README_ja.md) 4 | 5 | ### Summary 6 | * Loading SVG data on the browser via file or clipboard. 7 | * Applying physical effects to it. 8 | * Putting out SVG data. 9 | 10 | ![description](https://github.com/shspage/simple-svg-physics-runner/blob/master/image/description.gif) 11 | 12 | * The initial value of **gravity** is 0 in version 1.1.2b or later. Press "g" for normal gravity. 13 | 14 | ### How to use 15 | * Hit "v" to load SVG code from clipboard. (Chrome 66 or later) 16 | alternatively, drag and drop SVG file to load. 17 | 18 | * It loads only closed paths. After loading, the objects filled with **black** stay on fixed position. 19 | Other objects move according to the law of physics, and draggable. 20 | 21 | * Hit "p" to toggle pause/play. 22 | * Hit "s" to export SVG file. 23 | * Hit "w" to toggle wireframes. 24 | * Hit "g" to toggle gravity 0/1. 25 | * Hit "a" to toggle air resistance normal/HIGH 26 | 27 | * "w", "g" and "a" can be set before loading. 28 | 29 | ---- 30 | ### Advanced usage (Illustrator) 31 | 32 | #### Control by LAYER name 33 | * If the layer name includes "wrap", objects that go out of the screen will come in from the opposite side of the screen. 34 | (I recommend setting gravity to 0 before use.) 35 | 36 | #### Control by GROUP name 37 | * If the group name starts with "chain", the objects in the group are chained vertically, anchored at the top. 38 | video - https://twitter.com/shspage/status/1757720353638674790 39 | * Similarly, if it starts with "bridge", the ends are fixed and connected horizontally. 40 | * Similarly, if it starts with "loop", it will be connected in a loop. (Connect with each nearest and farthest object.) 41 | * If the group name starts with "chain" and includes " as is"(it needs first space), the original objects will be connected while maintaining their positions. 42 | 43 | #### Control by OBJECT name 44 | * If the name contains "hangN" (N is a natural number), it will generate a line hanging from above. N is the length of the line. 45 | In addition, if "offsetN" (N: -0.5 to 0.5) is included, the position connected to the line will be shifted by the ratio to the vertical width. In addition, if "#N" (N is a 3-digit or 6-digit hexadecimal number) is included, the line color will be changed. If it contains "hidden", no line will be drawn. These words work even if they are in the name of a group or layer. 46 | 47 | ### Try using 48 | https://shspage.github.io/simple-svg-physics-runner/ 49 | 50 | ### Note: 51 | * Loading from the clipboard does not work with the http protocol (works with https or file) 52 | * Non-convex polygons in SVG data are automatically converted to a group of convex polygons, 53 | and when you save them as SVG, they are exported as the original SVG shapes. 54 | Splitting into convex polygons may not work well for complex shapes. 55 | * **IMPORTANT** : In Illustrator, the original path may be changed automatically when SVG code is generated. 56 | For example, **a line whose "line position" is not "center"** is converted to a compound path with fill color of original line color. 57 | * When stacking objects are output as SVG, you may notice where the edges of the objects dig into each other. 58 | If there is enough processing power of your computer, you can mitigate this by increasing ENGINE_POSITION_ITERATIONS at the beginning of simple-svg-physics-runner.js. 59 | (Though I'm not sure whether it is appropriate to adjust this value to improve accuracy. 60 | Either way, I guess, for a physics engine, some error may be necessary for stability of processing.) 61 | There are some other constants you can change in the above script beginning part. 62 | However, it does not cover all attributes. 63 | For the details about attributes, please refer to Matter.js document. 64 | 65 | * In Illustrator, black (K100) in CMYK mode is not converted to #000 in the generated SVG code. 66 | This means the object doesn't stay on fixed position. 67 | So, if RGB is (35,24,21) or lower respectively, it is considered black and converted to #000, 68 | and it is also set to #000 when exporting. 69 | If you do not want to perform this color conversion, please set AS_BLACK_THRESHOLD_RGB to [0,0,0] 70 | in simple-svg-physics-runner.js. 71 | 72 | * Compound paths are drawn as shapes with no "holes". What looks like a hole is a shape painted with the background color. Therefore, you cannot place other shapes inside the "hole". 73 | 74 | 75 | ### ChangeLog 76 | #### v.1.2.0b2 77 | * Fixed lines generated by "hang" to be exported. 78 | 79 | #### v.1.2.0b1 80 | * Added control using "hang" etc. in object name. 81 | * If "data-name" is in attributes of an SVG element, it will be used instead of "id". 82 | 83 | #### v.1.2.0b 84 | * Added processing to convert curves to polylines as appropriate. 85 | * Exports the original SVG shapes, even if polygons and curves are reshaped to handle with the physics engine. 86 | 87 | #### v.1.1.2b1 88 | * Changed the text display to red when gravity is 0. Changed normal gravity value to 1. 89 | * Set the restitution value to all bodies. 90 | * Tentative fix for the problem where the last constraint of "chain" and "bridge" is hard. 91 | 92 | #### v.1.1.2b 93 | * Set default gravity to 0, normal gravity to 0.8. 94 | * Added "wrap" feature. (matter-wrap plugin appears to be working despite the required version unmatch) 95 | * Supports drawing compound paths. (See also note section for additional notes.) 96 | 97 | #### v.1.1.1 98 | * Illustrator: Added a function to connect objects in a loop when the group name starts with "loop". 99 | * Illustrator: If the group name starts with "chain" and includes " as is"(it needs first space), the original objects will be connected while maintaining their positions. 100 | 101 | #### v.1.1.0 102 | * updated required libraries. 103 | * removed canvas scrollbars. 104 | * Illustrator: Added a function to connect objects in the horizontal direction when the group name starts with "bridge". 105 | 106 | #### v.1.0.2 107 | * Added a setting of upper limit of RGB values ​​to be considered as black. (AS_BLACK_THRESHOLD_RGB) 108 | * Illustrator: Added a function to connect objects in the vertical direction when the group name starts with "chain". 109 | 110 | #### v.1.0.1 111 | * A path is regarded as a circle if the difference between the width and the height and the difference between the circumference / area calculated from the width as the diameter and the actual value is less than the set value. 112 | (This is for SVG files created with Inkscape. Classification methods may need further consideration.) 113 | * Adjusting the upper left corner of the loaded figure to be at the upper left of the canvas. 114 | 115 | ### TODO 116 | * Loading from the clipboard with firefox 117 | * flattening curves including ellipses automatically 118 | 119 | ### License 120 | * Copyright(c) 2018 Hiroyuki Sato 121 | https://github.com/shspage/simple-svg-physics-runner 122 | This software is distributed under the MIT License. 123 | See the LICENSE.txt for details. 124 | 125 | This software uses the following libraries that may have licenses 126 | differing from that of the software itself. You can find the 127 | libraries and their respective licenses below. 128 | 129 | #### required libraries (including in this repo) 130 | * jQuery (v3.7.1) 131 | License MIT 132 | (c) OpenJS Foundation and other contributors | jquery.org/license 133 | 134 | * matter-js (0.19.0) http://brm.io/matter-js/ 135 | License MIT 136 | Copyright (c) Liam Brummitt and contributors. 137 | 138 | * Paper.js (v0.12.17) http://paperjs.org/ 139 | License MIT 140 | Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 141 | http://juerglehni.com/ & https://puckey.studio/ 142 | 143 | * decomp.js (https://github.com/schteppe/poly-decomp.js) 144 | License MIT 145 | Copyright (c) 2013 Stefan Hedman 146 | -------------------------------------------------------------------------------- /js/lib/matter-attractors.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * matter-attractors 0.1.6 by Liam Brummitt 2017-05-15 3 | * https://github.com/liabru/matter-attractors 4 | * License MIT 5 | */ 6 | (function webpackUniversalModuleDefinition(root, factory) { 7 | if(typeof exports === 'object' && typeof module === 'object') 8 | module.exports = factory(require("matter-js")); 9 | else if(typeof define === 'function' && define.amd) 10 | define(["matter-js"], factory); 11 | else if(typeof exports === 'object') 12 | exports["MatterAttractors"] = factory(require("matter-js")); 13 | else 14 | root["MatterAttractors"] = factory(root["Matter"]); 15 | })(this, function(__WEBPACK_EXTERNAL_MODULE_0__) { 16 | return /******/ (function(modules) { // webpackBootstrap 17 | /******/ // The module cache 18 | /******/ var installedModules = {}; 19 | 20 | /******/ // The require function 21 | /******/ function __webpack_require__(moduleId) { 22 | 23 | /******/ // Check if module is in cache 24 | /******/ if(installedModules[moduleId]) 25 | /******/ return installedModules[moduleId].exports; 26 | 27 | /******/ // Create a new module (and put it into the cache) 28 | /******/ var module = installedModules[moduleId] = { 29 | /******/ i: moduleId, 30 | /******/ l: false, 31 | /******/ exports: {} 32 | /******/ }; 33 | 34 | /******/ // Execute the module function 35 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 36 | 37 | /******/ // Flag the module as loaded 38 | /******/ module.l = true; 39 | 40 | /******/ // Return the exports of the module 41 | /******/ return module.exports; 42 | /******/ } 43 | 44 | 45 | /******/ // expose the modules object (__webpack_modules__) 46 | /******/ __webpack_require__.m = modules; 47 | 48 | /******/ // expose the module cache 49 | /******/ __webpack_require__.c = installedModules; 50 | 51 | /******/ // identity function for calling harmony imports with the correct context 52 | /******/ __webpack_require__.i = function(value) { return value; }; 53 | 54 | /******/ // define getter function for harmony exports 55 | /******/ __webpack_require__.d = function(exports, name, getter) { 56 | /******/ if(!__webpack_require__.o(exports, name)) { 57 | /******/ Object.defineProperty(exports, name, { 58 | /******/ configurable: false, 59 | /******/ enumerable: true, 60 | /******/ get: getter 61 | /******/ }); 62 | /******/ } 63 | /******/ }; 64 | 65 | /******/ // getDefaultExport function for compatibility with non-harmony modules 66 | /******/ __webpack_require__.n = function(module) { 67 | /******/ var getter = module && module.__esModule ? 68 | /******/ function getDefault() { return module['default']; } : 69 | /******/ function getModuleExports() { return module; }; 70 | /******/ __webpack_require__.d(getter, 'a', getter); 71 | /******/ return getter; 72 | /******/ }; 73 | 74 | /******/ // Object.prototype.hasOwnProperty.call 75 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 76 | 77 | /******/ // __webpack_public_path__ 78 | /******/ __webpack_require__.p = "/libs"; 79 | 80 | /******/ // Load entry module and return exports 81 | /******/ return __webpack_require__(__webpack_require__.s = 1); 82 | /******/ }) 83 | /************************************************************************/ 84 | /******/ ([ 85 | /* 0 */ 86 | /***/ (function(module, exports) { 87 | 88 | module.exports = __WEBPACK_EXTERNAL_MODULE_0__; 89 | 90 | /***/ }), 91 | /* 1 */ 92 | /***/ (function(module, exports, __webpack_require__) { 93 | 94 | "use strict"; 95 | 96 | 97 | var Matter = __webpack_require__(0); 98 | 99 | /** 100 | * An attractors plugin for matter.js. 101 | * See the readme for usage and examples. 102 | * @module MatterAttractors 103 | */ 104 | var MatterAttractors = { 105 | // plugin meta 106 | name: 'matter-attractors', // PLUGIN_NAME 107 | version: '0.1.4', // PLUGIN_VERSION 108 | for: 'matter-js@^0.12.0', 109 | 110 | // installs the plugin where `base` is `Matter` 111 | // you should not need to call this directly. 112 | install: function install(base) { 113 | base.after('Body.create', function () { 114 | MatterAttractors.Body.init(this); 115 | }); 116 | 117 | base.before('Engine.update', function (engine) { 118 | MatterAttractors.Engine.update(engine); 119 | }); 120 | }, 121 | 122 | Body: { 123 | /** 124 | * Initialises the `body` to support attractors. 125 | * This is called automatically by the plugin. 126 | * @function MatterAttractors.Body.init 127 | * @param {Matter.Body} body The body to init. 128 | * @returns {void} No return value. 129 | */ 130 | init: function init(body) { 131 | body.plugin.attractors = body.plugin.attractors || []; 132 | } 133 | }, 134 | 135 | Engine: { 136 | /** 137 | * Applies all attractors for all bodies in the `engine`. 138 | * This is called automatically by the plugin. 139 | * @function MatterAttractors.Engine.update 140 | * @param {Matter.Engine} engine The engine to update. 141 | * @returns {void} No return value. 142 | */ 143 | update: function update(engine) { 144 | var world = engine.world, 145 | bodies = Matter.Composite.allBodies(world); 146 | 147 | for (var i = 0; i < bodies.length; i += 1) { 148 | var bodyA = bodies[i], 149 | attractors = bodyA.plugin.attractors; 150 | 151 | if (attractors && attractors.length > 0) { 152 | for (var j = i + 1; j < bodies.length; j += 1) { 153 | var bodyB = bodies[j]; 154 | 155 | for (var k = 0; k < attractors.length; k += 1) { 156 | var attractor = attractors[k], 157 | forceVector = attractor; 158 | 159 | if (Matter.Common.isFunction(attractor)) { 160 | forceVector = attractor(bodyA, bodyB); 161 | } 162 | 163 | if (forceVector) { 164 | Matter.Body.applyForce(bodyB, bodyB.position, forceVector); 165 | } 166 | } 167 | } 168 | } 169 | } 170 | } 171 | }, 172 | 173 | /** 174 | * Defines some useful common attractor functions that can be used 175 | * by pushing them to your body's `body.plugin.attractors` array. 176 | * @namespace MatterAttractors.Attractors 177 | * @property {number} gravityConstant The gravitational constant used by the gravity attractor. 178 | */ 179 | Attractors: { 180 | gravityConstant: 0.001, 181 | 182 | /** 183 | * An attractor function that applies Newton's law of gravitation. 184 | * Use this by pushing `MatterAttractors.Attractors.gravity` to your body's `body.plugin.attractors` array. 185 | * The gravitational constant defaults to `0.001` which you can change 186 | * at `MatterAttractors.Attractors.gravityConstant`. 187 | * @function MatterAttractors.Attractors.gravity 188 | * @param {Matter.Body} bodyA The first body. 189 | * @param {Matter.Body} bodyB The second body. 190 | * @returns {void} No return value. 191 | */ 192 | gravity: function gravity(bodyA, bodyB) { 193 | // use Newton's law of gravitation 194 | var bToA = Matter.Vector.sub(bodyB.position, bodyA.position), 195 | distanceSq = Matter.Vector.magnitudeSquared(bToA) || 0.0001, 196 | normal = Matter.Vector.normalise(bToA), 197 | magnitude = -MatterAttractors.Attractors.gravityConstant * (bodyA.mass * bodyB.mass / distanceSq), 198 | force = Matter.Vector.mult(normal, magnitude); 199 | 200 | // to apply forces to both bodies 201 | Matter.Body.applyForce(bodyA, bodyA.position, Matter.Vector.neg(force)); 202 | Matter.Body.applyForce(bodyB, bodyB.position, force); 203 | } 204 | } 205 | }; 206 | 207 | Matter.Plugin.register(MatterAttractors); 208 | 209 | module.exports = MatterAttractors; 210 | 211 | /** 212 | * @namespace Matter.Body 213 | * @see http://brm.io/matter-js/docs/classes/Body.html 214 | */ 215 | 216 | /** 217 | * This plugin adds a new property `body.plugin.attractors` to instances of `Matter.Body`. 218 | * This is an array of callback functions that will be called automatically 219 | * for every pair of bodies, on every engine update. 220 | * @property {Function[]} body.plugin.attractors 221 | * @memberof Matter.Body 222 | */ 223 | 224 | /** 225 | * An attractor function calculates the force to be applied 226 | * to `bodyB`, it should either: 227 | * - return the force vector to be applied to `bodyB` 228 | * - or apply the force to the body(s) itself 229 | * @callback AttractorFunction 230 | * @param {Matter.Body} bodyA 231 | * @param {Matter.Body} bodyB 232 | * @returns {Vector|undefined} a force vector (optional) 233 | */ 234 | 235 | /***/ }) 236 | /******/ ]); 237 | }); -------------------------------------------------------------------------------- /js/lib/matter-wrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * matter-wrap 0.2.0 by Liam Brummitt 2017-07-04 3 | * https://github.com/liabru/matter-wrap 4 | * License MIT 5 | */ 6 | (function webpackUniversalModuleDefinition(root, factory) { 7 | if(typeof exports === 'object' && typeof module === 'object') 8 | module.exports = factory(require("matter-js")); 9 | else if(typeof define === 'function' && define.amd) 10 | define(["matter-js"], factory); 11 | else if(typeof exports === 'object') 12 | exports["MatterWrap"] = factory(require("matter-js")); 13 | else 14 | root["MatterWrap"] = factory(root["Matter"]); 15 | })(this, function(__WEBPACK_EXTERNAL_MODULE_0__) { 16 | return /******/ (function(modules) { // webpackBootstrap 17 | /******/ // The module cache 18 | /******/ var installedModules = {}; 19 | 20 | /******/ // The require function 21 | /******/ function __webpack_require__(moduleId) { 22 | 23 | /******/ // Check if module is in cache 24 | /******/ if(installedModules[moduleId]) 25 | /******/ return installedModules[moduleId].exports; 26 | 27 | /******/ // Create a new module (and put it into the cache) 28 | /******/ var module = installedModules[moduleId] = { 29 | /******/ i: moduleId, 30 | /******/ l: false, 31 | /******/ exports: {} 32 | /******/ }; 33 | 34 | /******/ // Execute the module function 35 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 36 | 37 | /******/ // Flag the module as loaded 38 | /******/ module.l = true; 39 | 40 | /******/ // Return the exports of the module 41 | /******/ return module.exports; 42 | /******/ } 43 | 44 | 45 | /******/ // expose the modules object (__webpack_modules__) 46 | /******/ __webpack_require__.m = modules; 47 | 48 | /******/ // expose the module cache 49 | /******/ __webpack_require__.c = installedModules; 50 | 51 | /******/ // identity function for calling harmony imports with the correct context 52 | /******/ __webpack_require__.i = function(value) { return value; }; 53 | 54 | /******/ // define getter function for harmony exports 55 | /******/ __webpack_require__.d = function(exports, name, getter) { 56 | /******/ if(!__webpack_require__.o(exports, name)) { 57 | /******/ Object.defineProperty(exports, name, { 58 | /******/ configurable: false, 59 | /******/ enumerable: true, 60 | /******/ get: getter 61 | /******/ }); 62 | /******/ } 63 | /******/ }; 64 | 65 | /******/ // getDefaultExport function for compatibility with non-harmony modules 66 | /******/ __webpack_require__.n = function(module) { 67 | /******/ var getter = module && module.__esModule ? 68 | /******/ function getDefault() { return module['default']; } : 69 | /******/ function getModuleExports() { return module; }; 70 | /******/ __webpack_require__.d(getter, 'a', getter); 71 | /******/ return getter; 72 | /******/ }; 73 | 74 | /******/ // Object.prototype.hasOwnProperty.call 75 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 76 | 77 | /******/ // __webpack_public_path__ 78 | /******/ __webpack_require__.p = "/libs"; 79 | 80 | /******/ // Load entry module and return exports 81 | /******/ return __webpack_require__(__webpack_require__.s = 1); 82 | /******/ }) 83 | /************************************************************************/ 84 | /******/ ([ 85 | /* 0 */ 86 | /***/ (function(module, exports) { 87 | 88 | module.exports = __WEBPACK_EXTERNAL_MODULE_0__; 89 | 90 | /***/ }), 91 | /* 1 */ 92 | /***/ (function(module, exports, __webpack_require__) { 93 | 94 | "use strict"; 95 | 96 | 97 | var Matter = __webpack_require__(0); 98 | 99 | /** 100 | * A coordinate wrapping plugin for matter.js. 101 | * See the readme for usage and examples. 102 | * @module MatterWrap 103 | */ 104 | var MatterWrap = { 105 | // plugin meta 106 | name: 'matter-wrap', // PLUGIN_NAME 107 | version: '0.1.3', // PLUGIN_VERSION 108 | for: 'matter-js@^0.12.0', 109 | 110 | // installs the plugin where `base` is `Matter` 111 | // you should not need to call this directly. 112 | install: function install(base) { 113 | base.after('Engine.update', function () { 114 | MatterWrap.Engine.update(this); 115 | }); 116 | }, 117 | 118 | Engine: { 119 | /** 120 | * Updates the engine by wrapping bodies and composites inside `engine.world`. 121 | * This is called automatically by the plugin. 122 | * @function MatterWrap.Engine.update 123 | * @param {Matter.Engine} engine The engine to update. 124 | * @returns {void} No return value. 125 | */ 126 | update: function update(engine) { 127 | var world = engine.world, 128 | bodies = Matter.Composite.allBodies(world), 129 | composites = Matter.Composite.allComposites(world); 130 | 131 | for (var i = 0; i < bodies.length; i += 1) { 132 | var body = bodies[i]; 133 | 134 | if (body.plugin.wrap) { 135 | MatterWrap.Body.wrap(body, body.plugin.wrap); 136 | } 137 | } 138 | 139 | for (i = 0; i < composites.length; i += 1) { 140 | var composite = composites[i]; 141 | 142 | if (composite.plugin.wrap) { 143 | MatterWrap.Composite.wrap(composite, composite.plugin.wrap); 144 | } 145 | } 146 | } 147 | }, 148 | 149 | Bounds: { 150 | /** 151 | * Returns a translation vector that wraps the `objectBounds` inside the `bounds`. 152 | * @function MatterWrap.Bounds.wrap 153 | * @param {Matter.Bounds} objectBounds The bounds of the object to wrap inside the bounds. 154 | * @param {Matter.Bounds} bounds The bounds to wrap the body inside. 155 | * @returns {?Matter.Vector} A translation vector (only if wrapping is required). 156 | */ 157 | wrap: function wrap(objectBounds, bounds) { 158 | var x = null, 159 | y = null; 160 | 161 | if (typeof bounds.min.x !== 'undefined' && typeof bounds.max.x !== 'undefined') { 162 | if (objectBounds.min.x > bounds.max.x) { 163 | x = bounds.min.x - objectBounds.max.x; 164 | } else if (objectBounds.max.x < bounds.min.x) { 165 | x = bounds.max.x - objectBounds.min.x; 166 | } 167 | } 168 | 169 | if (typeof bounds.min.y !== 'undefined' && typeof bounds.max.y !== 'undefined') { 170 | if (objectBounds.min.y > bounds.max.y) { 171 | y = bounds.min.y - objectBounds.max.y; 172 | } else if (objectBounds.max.y < bounds.min.y) { 173 | y = bounds.max.y - objectBounds.min.y; 174 | } 175 | } 176 | 177 | if (x !== null || y !== null) { 178 | return { 179 | x: x || 0, 180 | y: y || 0 181 | }; 182 | } 183 | } 184 | }, 185 | 186 | Body: { 187 | /** 188 | * Wraps the `body` position such that it always stays within the given bounds. 189 | * Upon crossing a boundary the body will appear on the opposite side of the bounds, 190 | * while maintaining its velocity. 191 | * This is called automatically by the plugin. 192 | * @function MatterWrap.Body.wrap 193 | * @param {Matter.Body} body The body to wrap. 194 | * @param {Matter.Bounds} bounds The bounds to wrap the body inside. 195 | * @returns {?Matter.Vector} The translation vector that was applied (only if wrapping was required). 196 | */ 197 | wrap: function wrap(body, bounds) { 198 | var translation = MatterWrap.Bounds.wrap(body.bounds, bounds); 199 | 200 | if (translation) { 201 | Matter.Body.translate(body, translation); 202 | } 203 | 204 | return translation; 205 | } 206 | }, 207 | 208 | Composite: { 209 | /** 210 | * Returns the union of the bounds of all of the composite's bodies 211 | * (not accounting for constraints). 212 | * @function MatterWrap.Composite.bounds 213 | * @param {Matter.Composite} composite The composite. 214 | * @returns {Matter.Bounds} The composite bounds. 215 | */ 216 | bounds: function bounds(composite) { 217 | var bodies = Matter.Composite.allBodies(composite), 218 | vertices = []; 219 | 220 | for (var i = 0; i < bodies.length; i += 1) { 221 | var body = bodies[i]; 222 | vertices.push(body.bounds.min, body.bounds.max); 223 | } 224 | 225 | return Matter.Bounds.create(vertices); 226 | }, 227 | 228 | /** 229 | * Wraps the `composite` position such that it always stays within the given bounds. 230 | * Upon crossing a boundary the composite will appear on the opposite side of the bounds, 231 | * while maintaining its velocity. 232 | * This is called automatically by the plugin. 233 | * @function MatterWrap.Composite.wrap 234 | * @param {Matter.Composite} composite The composite to wrap. 235 | * @param {Matter.Bounds} bounds The bounds to wrap the composite inside. 236 | * @returns {?Matter.Vector} The translation vector that was applied (only if wrapping was required). 237 | */ 238 | wrap: function wrap(composite, bounds) { 239 | var translation = MatterWrap.Bounds.wrap(MatterWrap.Composite.bounds(composite), bounds); 240 | 241 | if (translation) { 242 | Matter.Composite.translate(composite, translation); 243 | } 244 | 245 | return translation; 246 | } 247 | } 248 | }; 249 | 250 | Matter.Plugin.register(MatterWrap); 251 | 252 | module.exports = MatterWrap; 253 | 254 | /** 255 | * @namespace Matter.Body 256 | * @see http://brm.io/matter-js/docs/classes/Body.html 257 | */ 258 | 259 | /** 260 | * This plugin adds a new property `body.plugin.wrap` to instances of `Matter.Body`. 261 | * This is a `Matter.Bounds` instance that specifies the wrapping region. 262 | * @property {Matter.Bounds} body.plugin.wrap 263 | * @memberof Matter.Body 264 | */ 265 | 266 | /** 267 | * This plugin adds a new property `composite.plugin.wrap` to instances of `Matter.Composite`. 268 | * This is a `Matter.Bounds` instance that specifies the wrapping region. 269 | * @property {Matter.Bounds} composite.plugin.wrap 270 | * @memberof Matter.Composite 271 | */ 272 | 273 | /***/ }) 274 | /******/ ]); 275 | }); -------------------------------------------------------------------------------- /js/lib/decomp.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.decomp=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=0 && s<=1 && t>=0 && t<=1); 61 | } 62 | 63 | /** 64 | * Get the area of a triangle spanned by the three given points. Note that the area will be negative if the points are not given in counter-clockwise order. 65 | * @static 66 | * @method area 67 | * @param {Array} a 68 | * @param {Array} b 69 | * @param {Array} c 70 | * @return {Number} 71 | */ 72 | function triangleArea(a,b,c){ 73 | return (((b[0] - a[0])*(c[1] - a[1]))-((c[0] - a[0])*(b[1] - a[1]))); 74 | } 75 | 76 | function isLeft(a,b,c){ 77 | return triangleArea(a,b,c) > 0; 78 | } 79 | 80 | function isLeftOn(a,b,c) { 81 | return triangleArea(a, b, c) >= 0; 82 | } 83 | 84 | function isRight(a,b,c) { 85 | return triangleArea(a, b, c) < 0; 86 | } 87 | 88 | function isRightOn(a,b,c) { 89 | return triangleArea(a, b, c) <= 0; 90 | } 91 | 92 | var tmpPoint1 = [], 93 | tmpPoint2 = []; 94 | 95 | /** 96 | * Check if three points are collinear 97 | * @method collinear 98 | * @param {Array} a 99 | * @param {Array} b 100 | * @param {Array} c 101 | * @param {Number} [thresholdAngle=0] Threshold angle to use when comparing the vectors. The function will return true if the angle between the resulting vectors is less than this value. Use zero for max precision. 102 | * @return {Boolean} 103 | */ 104 | function collinear(a,b,c,thresholdAngle) { 105 | if(!thresholdAngle){ 106 | return triangleArea(a, b, c) === 0; 107 | } else { 108 | var ab = tmpPoint1, 109 | bc = tmpPoint2; 110 | 111 | ab[0] = b[0]-a[0]; 112 | ab[1] = b[1]-a[1]; 113 | bc[0] = c[0]-b[0]; 114 | bc[1] = c[1]-b[1]; 115 | 116 | var dot = ab[0]*bc[0] + ab[1]*bc[1], 117 | magA = Math.sqrt(ab[0]*ab[0] + ab[1]*ab[1]), 118 | magB = Math.sqrt(bc[0]*bc[0] + bc[1]*bc[1]), 119 | angle = Math.acos(dot/(magA*magB)); 120 | return angle < thresholdAngle; 121 | } 122 | } 123 | 124 | function sqdist(a,b){ 125 | var dx = b[0] - a[0]; 126 | var dy = b[1] - a[1]; 127 | return dx * dx + dy * dy; 128 | } 129 | 130 | /** 131 | * Get a vertex at position i. It does not matter if i is out of bounds, this function will just cycle. 132 | * @method at 133 | * @param {Number} i 134 | * @return {Array} 135 | */ 136 | function polygonAt(polygon, i){ 137 | var s = polygon.length; 138 | return polygon[i < 0 ? i % s + s : i % s]; 139 | } 140 | 141 | /** 142 | * Clear the polygon data 143 | * @method clear 144 | * @return {Array} 145 | */ 146 | function polygonClear(polygon){ 147 | polygon.length = 0; 148 | } 149 | 150 | /** 151 | * Append points "from" to "to"-1 from an other polygon "poly" onto this one. 152 | * @method append 153 | * @param {Polygon} poly The polygon to get points from. 154 | * @param {Number} from The vertex index in "poly". 155 | * @param {Number} to The end vertex index in "poly". Note that this vertex is NOT included when appending. 156 | * @return {Array} 157 | */ 158 | function polygonAppend(polygon, poly, from, to){ 159 | for(var i=from; i v[br][0])) { 175 | br = i; 176 | } 177 | } 178 | 179 | // reverse poly if clockwise 180 | if (!isLeft(polygonAt(polygon, br - 1), polygonAt(polygon, br), polygonAt(polygon, br + 1))) { 181 | polygonReverse(polygon); 182 | } 183 | } 184 | 185 | /** 186 | * Reverse the vertices in the polygon 187 | * @method reverse 188 | */ 189 | function polygonReverse(polygon){ 190 | var tmp = []; 191 | var N = polygon.length; 192 | for(var i=0; i!==N; i++){ 193 | tmp.push(polygon.pop()); 194 | } 195 | for(var i=0; i!==N; i++){ 196 | polygon[i] = tmp[i]; 197 | } 198 | } 199 | 200 | /** 201 | * Check if a point in the polygon is a reflex point 202 | * @method isReflex 203 | * @param {Number} i 204 | * @return {Boolean} 205 | */ 206 | function polygonIsReflex(polygon, i){ 207 | return isRight(polygonAt(polygon, i - 1), polygonAt(polygon, i), polygonAt(polygon, i + 1)); 208 | } 209 | 210 | var tmpLine1=[], 211 | tmpLine2=[]; 212 | 213 | /** 214 | * Check if two vertices in the polygon can see each other 215 | * @method canSee 216 | * @param {Number} a Vertex index 1 217 | * @param {Number} b Vertex index 2 218 | * @return {Boolean} 219 | */ 220 | function polygonCanSee(polygon, a,b) { 221 | var p, dist, l1=tmpLine1, l2=tmpLine2; 222 | 223 | if (isLeftOn(polygonAt(polygon, a + 1), polygonAt(polygon, a), polygonAt(polygon, b)) && isRightOn(polygonAt(polygon, a - 1), polygonAt(polygon, a), polygonAt(polygon, b))) { 224 | return false; 225 | } 226 | dist = sqdist(polygonAt(polygon, a), polygonAt(polygon, b)); 227 | for (var i = 0; i !== polygon.length; ++i) { // for each edge 228 | if ((i + 1) % polygon.length === a || i === a){ // ignore incident edges 229 | continue; 230 | } 231 | if (isLeftOn(polygonAt(polygon, a), polygonAt(polygon, b), polygonAt(polygon, i + 1)) && isRightOn(polygonAt(polygon, a), polygonAt(polygon, b), polygonAt(polygon, i))) { // if diag intersects an edge 232 | l1[0] = polygonAt(polygon, a); 233 | l1[1] = polygonAt(polygon, b); 234 | l2[0] = polygonAt(polygon, i); 235 | l2[1] = polygonAt(polygon, i + 1); 236 | p = lineInt(l1,l2); 237 | if (sqdist(polygonAt(polygon, a), p) < dist) { // if edge is blocking visibility to b 238 | return false; 239 | } 240 | } 241 | } 242 | 243 | return true; 244 | } 245 | 246 | /** 247 | * Copy the polygon from vertex i to vertex j. 248 | * @method copy 249 | * @param {Number} i 250 | * @param {Number} j 251 | * @param {Polygon} [targetPoly] Optional target polygon to save in. 252 | * @return {Polygon} The resulting copy. 253 | */ 254 | function polygonCopy(polygon, i,j,targetPoly){ 255 | var p = targetPoly || []; 256 | polygonClear(p); 257 | if (i < j) { 258 | // Insert all vertices from i to j 259 | for(var k=i; k<=j; k++){ 260 | p.push(polygon[k]); 261 | } 262 | 263 | } else { 264 | 265 | // Insert vertices 0 to j 266 | for(var k=0; k<=j; k++){ 267 | p.push(polygon[k]); 268 | } 269 | 270 | // Insert vertices i to end 271 | for(var k=i; k 0){ 321 | return polygonSlice(polygon, edges); 322 | } else { 323 | return [polygon]; 324 | } 325 | } 326 | 327 | /** 328 | * Slices the polygon given one or more cut edges. If given one, this function will return two polygons (false on failure). If many, an array of polygons. 329 | * @method slice 330 | * @param {Array} cutEdges A list of edges, as returned by .getCutEdges() 331 | * @return {Array} 332 | */ 333 | function polygonSlice(polygon, cutEdges){ 334 | if(cutEdges.length === 0){ 335 | return [polygon]; 336 | } 337 | if(cutEdges instanceof Array && cutEdges.length && cutEdges[0] instanceof Array && cutEdges[0].length===2 && cutEdges[0][0] instanceof Array){ 338 | 339 | var polys = [polygon]; 340 | 341 | for(var i=0; i maxlevel){ 450 | console.warn("quickDecomp: max level ("+maxlevel+") reached."); 451 | return result; 452 | } 453 | 454 | for (var i = 0; i < polygon.length; ++i) { 455 | if (polygonIsReflex(poly, i)) { 456 | reflexVertices.push(poly[i]); 457 | upperDist = lowerDist = Number.MAX_VALUE; 458 | 459 | 460 | for (var j = 0; j < polygon.length; ++j) { 461 | if (isLeft(polygonAt(poly, i - 1), polygonAt(poly, i), polygonAt(poly, j)) && isRightOn(polygonAt(poly, i - 1), polygonAt(poly, i), polygonAt(poly, j - 1))) { // if line intersects with an edge 462 | p = getIntersectionPoint(polygonAt(poly, i - 1), polygonAt(poly, i), polygonAt(poly, j), polygonAt(poly, j - 1)); // find the point of intersection 463 | if (isRight(polygonAt(poly, i + 1), polygonAt(poly, i), p)) { // make sure it's inside the poly 464 | d = sqdist(poly[i], p); 465 | if (d < lowerDist) { // keep only the closest intersection 466 | lowerDist = d; 467 | lowerInt = p; 468 | lowerIndex = j; 469 | } 470 | } 471 | } 472 | if (isLeft(polygonAt(poly, i + 1), polygonAt(poly, i), polygonAt(poly, j + 1)) && isRightOn(polygonAt(poly, i + 1), polygonAt(poly, i), polygonAt(poly, j))) { 473 | p = getIntersectionPoint(polygonAt(poly, i + 1), polygonAt(poly, i), polygonAt(poly, j), polygonAt(poly, j + 1)); 474 | if (isLeft(polygonAt(poly, i - 1), polygonAt(poly, i), p)) { 475 | d = sqdist(poly[i], p); 476 | if (d < upperDist) { 477 | upperDist = d; 478 | upperInt = p; 479 | upperIndex = j; 480 | } 481 | } 482 | } 483 | } 484 | 485 | // if there are no vertices to connect to, choose a point in the middle 486 | if (lowerIndex === (upperIndex + 1) % polygon.length) { 487 | //console.log("Case 1: Vertex("+i+"), lowerIndex("+lowerIndex+"), upperIndex("+upperIndex+"), poly.size("+polygon.length+")"); 488 | p[0] = (lowerInt[0] + upperInt[0]) / 2; 489 | p[1] = (lowerInt[1] + upperInt[1]) / 2; 490 | steinerPoints.push(p); 491 | 492 | if (i < upperIndex) { 493 | //lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.begin() + upperIndex + 1); 494 | polygonAppend(lowerPoly, poly, i, upperIndex+1); 495 | lowerPoly.push(p); 496 | upperPoly.push(p); 497 | if (lowerIndex !== 0){ 498 | //upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.end()); 499 | polygonAppend(upperPoly, poly,lowerIndex,poly.length); 500 | } 501 | //upperPoly.insert(upperPoly.end(), poly.begin(), poly.begin() + i + 1); 502 | polygonAppend(upperPoly, poly,0,i+1); 503 | } else { 504 | if (i !== 0){ 505 | //lowerPoly.insert(lowerPoly.end(), poly.begin() + i, poly.end()); 506 | polygonAppend(lowerPoly, poly,i,poly.length); 507 | } 508 | //lowerPoly.insert(lowerPoly.end(), poly.begin(), poly.begin() + upperIndex + 1); 509 | polygonAppend(lowerPoly, poly,0,upperIndex+1); 510 | lowerPoly.push(p); 511 | upperPoly.push(p); 512 | //upperPoly.insert(upperPoly.end(), poly.begin() + lowerIndex, poly.begin() + i + 1); 513 | polygonAppend(upperPoly, poly,lowerIndex,i+1); 514 | } 515 | } else { 516 | // connect to the closest point within the triangle 517 | //console.log("Case 2: Vertex("+i+"), closestIndex("+closestIndex+"), poly.size("+polygon.length+")\n"); 518 | 519 | if (lowerIndex > upperIndex) { 520 | upperIndex += polygon.length; 521 | } 522 | closestDist = Number.MAX_VALUE; 523 | 524 | if(upperIndex < lowerIndex){ 525 | return result; 526 | } 527 | 528 | for (var j = lowerIndex; j <= upperIndex; ++j) { 529 | if (isLeftOn(polygonAt(poly, i - 1), polygonAt(poly, i), polygonAt(poly, j)) && isRightOn(polygonAt(poly, i + 1), polygonAt(poly, i), polygonAt(poly, j))) { 530 | d = sqdist(polygonAt(poly, i), polygonAt(poly, j)); 531 | if (d < closestDist) { 532 | closestDist = d; 533 | closestIndex = j % polygon.length; 534 | } 535 | } 536 | } 537 | 538 | if (i < closestIndex) { 539 | polygonAppend(lowerPoly, poly,i,closestIndex+1); 540 | if (closestIndex !== 0){ 541 | polygonAppend(upperPoly, poly,closestIndex,v.length); 542 | } 543 | polygonAppend(upperPoly, poly,0,i+1); 544 | } else { 545 | if (i !== 0){ 546 | polygonAppend(lowerPoly, poly,i,v.length); 547 | } 548 | polygonAppend(lowerPoly, poly,0,closestIndex+1); 549 | polygonAppend(upperPoly, poly,closestIndex,i+1); 550 | } 551 | } 552 | 553 | // solve smallest poly first 554 | if (lowerPoly.length < upperPoly.length) { 555 | polygonQuickDecomp(lowerPoly,result,reflexVertices,steinerPoints,delta,maxlevel,level); 556 | polygonQuickDecomp(upperPoly,result,reflexVertices,steinerPoints,delta,maxlevel,level); 557 | } else { 558 | polygonQuickDecomp(upperPoly,result,reflexVertices,steinerPoints,delta,maxlevel,level); 559 | polygonQuickDecomp(lowerPoly,result,reflexVertices,steinerPoints,delta,maxlevel,level); 560 | } 561 | 562 | return result; 563 | } 564 | } 565 | result.push(polygon); 566 | 567 | return result; 568 | } 569 | 570 | /** 571 | * Remove collinear points in the polygon. 572 | * @method removeCollinearPoints 573 | * @param {Number} [precision] The threshold angle to use when determining whether two edges are collinear. Use zero for finest precision. 574 | * @return {Number} The number of points removed 575 | */ 576 | function polygonRemoveCollinearPoints(polygon, precision){ 577 | var num = 0; 578 | for(var i=polygon.length-1; polygon.length>3 && i>=0; --i){ 579 | if(collinear(polygonAt(polygon, i-1),polygonAt(polygon, i),polygonAt(polygon, i+1),precision)){ 580 | // Remove the middle point 581 | polygon.splice(i%polygon.length,1); 582 | num++; 583 | } 584 | } 585 | return num; 586 | } 587 | 588 | /** 589 | * Check if two scalars are equal 590 | * @static 591 | * @method eq 592 | * @param {Number} a 593 | * @param {Number} b 594 | * @param {Number} [precision] 595 | * @return {Boolean} 596 | */ 597 | function scalar_eq(a,b,precision){ 598 | precision = precision || 0; 599 | return Math.abs(a-b) < precision; 600 | } 601 | 602 | },{}]},{},[1]) 603 | (1) 604 | }); 605 | -------------------------------------------------------------------------------- /js/simple-svg-physics-runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | simple-svg-physics-runner 3 | 4 | 2024.02.22, v.1.2.0b2 5 | 6 | * Copyright(c) 2018 Hiroyuki Sato 7 | https://github.com/shspage/simple-svg-physics-runner 8 | This software is distributed under the MIT License. 9 | See the LICENSE.txt for details. 10 | 11 | This software uses the following libraries that may have licenses 12 | differing from that of the software itself. You can find the 13 | libraries and their respective licenses below. 14 | ------------------------------------------- 15 | required libraries (and the version tested with) 16 | * jQuery (v3.7.1) 17 | License MIT 18 | (c) OpenJS Foundation and other contributors | jquery.org/license 19 | 20 | * matter-js (0.19.0) http://brm.io/matter-js/ 21 | License MIT 22 | Copyright (c) Liam Brummitt and contributors. 23 | 24 | * Paper.js (v0.12.17) http://paperjs.org/ 25 | License MIT 26 | Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey 27 | http://juerglehni.com/ & https://puckey.studio/ 28 | 29 | * decomp.js (https://github.com/schteppe/poly-decomp.js) 30 | License MIT 31 | Copyright (c) 2013 Stefan Hedman 32 | 33 | */ 34 | 35 | (function () { 36 | 'use strict'; 37 | Matter.use('matter-wrap'); 38 | console.log("@shspage: The wrap plugin appears to be working despite the required version unmatch"); 39 | 40 | const EXPORT_SVG_FILENAME = "output.svg"; 41 | const CANVAS_BACKGROUND_COLOR = "#fafafa"; 42 | const BODY_DENSITY = 0.12; // default=0.001 43 | const FRICTIONAIR_DEFAULT = 0.01; // 空気抵抗の既定値 44 | const FRICTIONAIR_HIGH = 0.8; // 空気抵抗=重い の値 45 | const GRAVITY_Y_DEFAULT = 1; // 重力=通常 の値 46 | const RESTITUTION_DEFAULT = 0.35; // 反発の既定値 47 | 48 | // Matter.Engine properties 49 | // * The higher the value, the higher quality the simulation will be at the expense of performance. 50 | // * 各値を増やすことでパフォーマンスと引き換えにシミュレーションの品質を上げることができる。 51 | // 各値がどのように影響するかは、Demo頁の右から引き出せるスライダ群を操作してみると手掛かりになるかも。 52 | const ENGINE_POSITION_ITERATIONS = 12; // positionIterations, default=6 53 | const ENGINE_VELOCITY_ITERATIONS = 4; // velocityIterations, default=4 54 | const ENGINE_CONSTRAINT_ITERATIONS = 2; // constraintIterations, default=2 55 | 56 | const DECOMP_MINIMUM_AREA = 0; // 複合パスを図形に分解した際に削除する図形の最大面積 57 | const DUPLICATE_POINT_TOLERANCE = 0.01; // 複合パスの重複点を取り除くときの許容差 58 | const REMOVE_COLLINEAR_PRECISION = 0.01; // 頂点が直線上にあると見なす許容差 59 | const FLATTEN_FLATNESS = 0.25; // 曲線を折れ線化する際の許容誤差 60 | 61 | // AS_BLACK_THRESHOLD_RGB: 62 | // * In Illustrator, black in CMYK mode is not converted to #000 in generated SVG code. 63 | // This means the object doesn't stay on fixed position. 64 | // So, set the upper limit of RGB values that is considered black. (0-255 = 0-1) 65 | // If all RGB values are below the upper limit, it will be converted to #000, 66 | // and it will also be #000 when exporting. 67 | // If you ignore CMYK mode, it's OK to set this [0,0,0]. 68 | // * イラレのCMYKモードでの黒(K100)は、生成されるSVGコードでは#000にならない。 69 | // つまり位置が固定されない。 70 | // 以下で黒と見なすRGBそれぞれの上限値を設定しておくと(0~255 を 0~1 とする)、 71 | // RGB全てが上限値以下の場合に#000に変換され、エクスポート時も#000になる。 72 | // CMYKモードを考慮しない場合は [0,0,0] に設定してもよい。 73 | const AS_BLACK_THRESHOLD_RGB = [0.1373, 0.0942, 0.0824]; // about #231815 74 | 75 | paper.setup("hidden_canvas"); 76 | 77 | var Body = Matter.Body, 78 | Bodies = Matter.Bodies, 79 | Vertices = Matter.Vertices, 80 | Vector = Matter.Vector, 81 | Mouse = Matter.Mouse, 82 | MouseConstraint = Matter.MouseConstraint, 83 | Render = Matter.Render, 84 | Runner = Matter.Runner, 85 | Constraint = Matter.Constraint, 86 | Composites = Matter.Composites, 87 | Composite = Matter.Composite, 88 | Engine = Matter.Engine, 89 | Common = Matter.Common; 90 | 91 | var _spec = { 92 | url:null, 93 | isPause:false, 94 | wireframes: false, 95 | gravity_y: 0, 96 | frictionAir:FRICTIONAIR_DEFAULT, 97 | _decomp_fail_count:0 98 | } 99 | 100 | // * Ratio to consider pass as circle (Larger ÷ Smaller) 101 | // bounds: width to height ratio 102 | // length, area : ratio between "the perimeter and area calculated using 103 | // the width as diameter" and "actual value" 104 | // * パスを円と見なす比率(大きい方÷小さい方) 105 | // bounds: 幅と高さの比率 106 | // perimeter, area : 幅を直径として算出した円の周長・面積と、実際の値との比率 107 | var _circleRatioLimit = { 108 | bounds : 1.02, 109 | perimeter : 1.05, 110 | area : 1.02 111 | } 112 | 113 | var _messages; 114 | 115 | var _engine; 116 | var _runner; 117 | var _render; 118 | 119 | var _svgShapes = {}; // { body.id : paper.Item } 120 | var _svgDataNames = {}; // { id : data-name } 121 | 122 | function setupWorld(items){ 123 | _engine = Engine.create({ 124 | positionIterations: ENGINE_POSITION_ITERATIONS, 125 | velocityIterations : ENGINE_VELOCITY_ITERATIONS, 126 | constraintIterations : ENGINE_CONSTRAINT_ITERATIONS 127 | }); 128 | _engine.world.gravity.y = _spec.gravity_y; 129 | 130 | _render = Render.create({ 131 | element: document.body, 132 | engine: _engine, 133 | options: { 134 | background: CANVAS_BACKGROUND_COLOR, 135 | width: window.innerWidth, 136 | height: window.innerHeight, 137 | showVelocity: false, 138 | wireframes: _spec.wireframes 139 | } 140 | }); 141 | 142 | Render.run(_render); 143 | 144 | _runner = Runner.create(); 145 | Runner.run(_runner, _engine); 146 | 147 | var mouse = Mouse.create(_render.canvas), 148 | mouseConstraint = MouseConstraint.create(_engine, { 149 | element: document.body, 150 | constraint: { 151 | stiffness: 0.2, 152 | render: { 153 | visible: false 154 | } 155 | } 156 | }); 157 | 158 | Composite.add(_engine.world, mouseConstraint); 159 | Composite.add(_engine.world, items); 160 | 161 | _render.mouse = mouse; 162 | 163 | Render.lookAt(_render, { 164 | min: { x: 0, y: 0 }, 165 | max: { x: window.innerWidth, y: window.innerHeight } 166 | }); 167 | } 168 | 169 | // ---------------------- 170 | // functions to load data 171 | // ---------------------- 172 | function checkLoaded(){ 173 | if(_engine){ 174 | alert(getMessage("already_loaded")); 175 | return true; 176 | } 177 | return false; 178 | } 179 | 180 | // drag and drop 181 | function handleFileSelect(evt){ 182 | evt.stopPropagation(); 183 | evt.preventDefault(); 184 | if(checkLoaded()) return; 185 | 186 | var files = evt.dataTransfer.files; 187 | if(files.length > 0){ 188 | var fileobj = files[0]; 189 | var type = fileobj.type; 190 | if(type == "image/svg+xml"){ 191 | var fr = new FileReader(); 192 | fr.onload = function(e){ 193 | importSVG_paper(e.target.result); 194 | } 195 | fr.readAsText(fileobj, "utf-8"); 196 | } else { 197 | if(type == "") type = "(unknown)"; 198 | alert(getMessage("please_select_a_svg") + type + ")."); 199 | } 200 | } 201 | } 202 | 203 | function handleDragOver(evt){ 204 | evt.stopPropagation(); 205 | evt.preventDefault(); 206 | evt.dataTransfer.dropEffect = "copy"; 207 | } 208 | 209 | // chrome 版。firefoxは追って対応かも(textareaを介して読み込む形になる) 210 | function importSVGFromClipboard_paper(){ 211 | if(checkLoaded()) return; 212 | if(navigator.clipboard){ 213 | navigator.clipboard.readText() 214 | .then((data) => importSVG_paper(data)) 215 | .catch((e) => alert(getMessage("paste_failed"))); 216 | } else { 217 | alert(getMessage("paste_chrome_only_for_now")); 218 | } 219 | } 220 | 221 | function importSVG_paper(data){ 222 | try{ 223 | _spec._decomp_fail_count = 0; 224 | 225 | var rex = /\<\w+ id=\"(\w+)\" data-name=\"([^\"]+)\"/g; 226 | var m; 227 | while((m = rex.exec(data)) !== null){ 228 | _svgDataNames[m[1]] = m[2]; 229 | } 230 | 231 | paper.project.view.viewSize = new paper.Size( 232 | window.innerWidth, window.innerHeight); 233 | 234 | paper.project.importSVG(data); 235 | fixTopLeftAfterImport(); 236 | paper2matter(); 237 | 238 | var msg = "LOADED"; 239 | if(_spec._decomp_fail_count > 0){ 240 | msg += " / decomp error (" + _spec._decomp_fail_count + ")"; 241 | } 242 | alert(msg); 243 | } catch(e){ 244 | alert(e); 245 | } 246 | } 247 | 248 | function fixTopLeftAfterImport(){ 249 | var items = paper.project.activeLayer.children; 250 | 251 | if(items.length > 0){ 252 | var top = items[0].bounds.top; 253 | var left = items[0].bounds.left; 254 | 255 | for(var i = 1, iEnd = items.length; i < iEnd; i++){ 256 | top = Math.min(top, items[i].bounds.top); 257 | left = Math.min(left, items[i].bounds.left); 258 | } 259 | 260 | if(top != 0 || left != 0){ 261 | var delta = new paper.Point(-left, -top); 262 | for(var i = 0, iEnd = items.length; i < iEnd; i++){ 263 | items[i].translate(delta); 264 | } 265 | } 266 | } 267 | } 268 | 269 | // ---------------------- 270 | // functions to convert data, from paper.js to Matter.js 271 | // ---------------------- 272 | // * Load SVG data from clipboard or file. Using "importSVG" of paper.js. 273 | // Matter.js also has SVG import method, but I couldn't find a sample 274 | // code to get color of each shape in SVG data. Since there may be 275 | // various format in SVG data, I thought that it is easier to handle 276 | // by putting it in the paper.js class rather than writing the analyzing 277 | // process by myself. For this reason, at here, the Paths loaded with 278 | // paper.js are converted to Body object of Matter.js. 279 | // Paths are kept in the background and used when exporting. 280 | // * クリップボードまたはファイルからのSVGデータ取り込み。 281 | // paper.js の importSVG で行う。Matter.js にも SVG 取り込みの 282 | // メソッドはあるが、サンプルコードにあるのは形状を取り込む処理 283 | // だけで、色を取得する処理がない。SVG のデータも色々なものが 284 | // あるので、自力で解析する処理を書くより、paper.js のクラスに 285 | // 落とし込んだほうが扱いやすいと考えた。 286 | // このためここでは、paper.js で取り込んだ Path を Matter.js の 287 | // Body に変換する処理をしている。 288 | // Path はバックグラウンドで保持され、エクスポート時に使われる。 289 | 290 | function paper2matter(){ 291 | var items = []; 292 | extractItems(paper.project.activeLayer.children, items, ""); 293 | setupWorld(items); 294 | } 295 | 296 | function extractItems(children, items, parent_name){ 297 | var grp, grp_type, is_wrap, coll_group; 298 | if(parent_name){ 299 | if(parent_name.startsWith("chain")){ 300 | grp = Composite.create(); 301 | grp_type = "chain"; 302 | } else if(parent_name.startsWith("bridge")){ 303 | grp = Composite.create(); 304 | grp_type = "bridge"; 305 | coll_group = Body.nextGroup(true); 306 | } else if(parent_name.startsWith("loop")){ 307 | grp = Composite.create(); 308 | grp_type = "loop"; 309 | coll_group = Body.nextGroup(true); 310 | } 311 | 312 | is_wrap = parent_name.includes("wrap"); 313 | } else { 314 | parent_name = ""; 315 | } 316 | 317 | for(var i = children.length - 1; i >= 0; i--){ 318 | var c = children[i]; 319 | var body; 320 | 321 | var cname = c.name || ""; 322 | if(cname != "" && cname in _svgDataNames){ 323 | cname = _svgDataNames[cname]; 324 | } 325 | 326 | if(c.className == 'Layer'){ 327 | extractItems(c.children, items, cname); 328 | } else if(c.className == 'Group'){ 329 | extractItems(c.children, items, cname + "." + parent_name); 330 | } else if(c.clipMask){ 331 | c.remove(); 332 | } else if(c.className == "Shape" || c.className == "Path" || c.className == "CompoundPath"){ 333 | if(c.className == "CompoundPath"){ 334 | if(!coll_group) coll_group = Body.nextGroup(true); 335 | body = createCompoundBody(c, coll_group); 336 | } else if(c.shape == "rectangle"){ 337 | body = createRectangle(c); 338 | } else if(c.shape == "circle" || isCircle(c)){ 339 | body = createCircle(c); 340 | } else if(c.shape == "ellipse"){ 341 | var tmp_path = c.toPath(); 342 | body = createPolygon(tmp_path); 343 | tmp_path.remove(); 344 | } else if(c.segments){ 345 | if(c.closed){ 346 | body = createPolygon(c); 347 | } 348 | } 349 | if(!body) continue; 350 | 351 | if(grp_type == "chain" || grp_type == "bridge" || grp_type == "loop"){ 352 | if(coll_group) body.collisionFilter.group = coll_group; 353 | Composite.addBody(grp, body); 354 | } else { 355 | if(!hangBody(cname, parent_name, body, items)){ 356 | if(is_wrap) addWrap(body); 357 | } 358 | items.push(body); 359 | } 360 | _svgShapes[body.id] = c; 361 | } 362 | } 363 | 364 | if(grp_type == "chain"){ 365 | createChain(grp, parent_name); 366 | items.push(grp); 367 | } else if(grp_type == "bridge"){ 368 | createBridge(grp); 369 | items.push(grp); 370 | } else if(grp_type == "loop"){ 371 | if(is_wrap) addWrap(grp); 372 | // create outer constraints 373 | grp.bodies = sortBodiesByNearest(grp.bodies); 374 | var stiffness = 0.2; 375 | createLoop(grp, stiffness); 376 | // create inner constraints 377 | if(grp.bodies.length > 3){ 378 | grp.bodies = sortBodiesByNearest(grp.bodies, true); 379 | var ignoreLast = true; 380 | stiffness = 0.2; 381 | createLoop(grp, stiffness, ignoreLast); 382 | } 383 | items.push(grp); 384 | } 385 | } 386 | 387 | function addWrap(body){ // body or composite 388 | body.plugin.wrap = { 389 | min: { 390 | x: 0, 391 | y: 0 392 | }, 393 | max: { 394 | x: window.innerWidth, 395 | y: window.innerWidth 396 | } 397 | }; 398 | } 399 | 400 | function hangBody(cname, parent_name, body, items){ 401 | var hanged = false; 402 | if(body.isStatic) return hanged; 403 | var name = cname + "." + parent_name; 404 | var m = name.match(/hang([0-9]+)/); 405 | if(m){ 406 | hanged = true; 407 | var len = m[1]; 408 | //Body.setMass(body, 100); 409 | //body.restitution = 1; 410 | //Body.setInertia(body, Infinity); 411 | //body.friction = 0; 412 | 413 | var height = body.bounds.max.y - body.bounds.min.y; 414 | 415 | var offset = 0; 416 | m = name.match(/offset(-?0\.[0-5])/i); 417 | if(m){ 418 | offset = height * m[1]; 419 | } 420 | 421 | var constraint = Constraint.create({ 422 | pointA: { x: body.position.x, y: body.position.y - len}, 423 | bodyB: body, 424 | pointB: { x: 0, y: offset }, 425 | }); 426 | 427 | if(name.includes("hidden")){ 428 | constraint.render.visible = false; 429 | } else { 430 | var strokeColor = "#000"; 431 | m = name.match(/#[0-9a-f]{6}/i); 432 | if(m){ 433 | strokeColor = m[0]; 434 | } else { 435 | m = name.match(/#[0-9a-f]{3}/i); 436 | if(m){ 437 | strokeColor = m[0]; 438 | } 439 | } 440 | constraint.render.type = "line"; 441 | constraint.render.strokeStyle = strokeColor; 442 | constraint.render.lineWidth = 1; 443 | constraint.render.anchors = false; 444 | constraint.label = "hang"; 445 | } 446 | items.push(constraint); 447 | } 448 | return hanged; 449 | } 450 | 451 | function createChain(grp, parent_name){ 452 | // sort bodies from top to bottom. the pivot of chain is placed at the top body. 453 | var as_is = parent_name.includes(" as is"); 454 | if(grp.bodies.length > 1){ 455 | grp.bodies.sort(function(a,b){ return a.position.y - b.position.y; }); 456 | if(as_is){ 457 | for(var i = 0, iEnd = grp.bodies.length - 1; i < iEnd; i++){ 458 | var b = grp.bodies[i]; 459 | var b1 = grp.bodies[i + 1] 460 | Composite.add(grp, Constraint.create({ 461 | bodyA: b, 462 | bodyB: b1, 463 | length: Vector.magnitude(Vector.sub(b.position, b1.position)), 464 | stiffness: 0.99, 465 | render: { visible:false } 466 | })) 467 | } 468 | } else { 469 | Composites.chain(grp, 0, 0.5, 0, -0.5, { stiffness: 0.99, length: 1, render: { visible:false } }); 470 | // 最後の constraint の動作が固い。原因は今のところ不明。 471 | grp.constraints[grp.constraints.length - 1].stiffness = 0.66; 472 | } 473 | } 474 | if(true){ 475 | var b = grp.bodies[0]; 476 | var bHeight = b.bounds.max.y - b.bounds.min.y 477 | Composite.add(grp, Constraint.create({ 478 | bodyB: b, 479 | pointB: { x: 0, y: -bHeight / 2 }, 480 | pointA: { x: b.position.x, y: b.position.y - bHeight / 2}, 481 | stiffness: 1, 482 | render: { visible:false } 483 | })); 484 | } 485 | } 486 | 487 | function createBridge(grp){ 488 | // sort bodies horizontaly. the pivot of chain is placed at both ends of bodies. 489 | if(grp.bodies.length < 1) return; 490 | 491 | if(grp.bodies.length > 1){ 492 | grp.bodies.sort(function(a,b){ return a.position.x - b.position.x; }); 493 | } 494 | var left = grp.bodies[0].bounds.min.x; 495 | var right = grp.bodies[grp.bodies.length - 1].bounds.max.x; 496 | if(grp.bodies.length > 1){ 497 | Composites.chain(grp, 0.5, 0, -0.5, 0, { stiffness: 0.99, length: 0.0001, render: { visible: false } }); 498 | // 最後の constraint の動作が固い。原因は今のところ不明。 499 | grp.constraints[grp.constraints.length - 1].stiffness = 0.66; 500 | } 501 | var b = grp.bodies[0]; 502 | var bWidth = b.bounds.max.x - b.bounds.min.x; 503 | Composite.add(grp, Constraint.create({ 504 | bodyB: b, 505 | pointB: { x: -bWidth / 2, y: 0 }, 506 | pointA: { x: left, y: b.position.y }, 507 | length: 2, 508 | stiffness: 0.9, 509 | render: { visible: false } 510 | })); 511 | b = grp.bodies[grp.bodies.length - 1]; 512 | bWidth = b.bounds.max.x - b.bounds.min.x; 513 | Composite.add(grp, Constraint.create({ 514 | bodyB: b, 515 | pointB: { x: bWidth / 2, y: 0 }, 516 | pointA: { x: right, y: b.position.y }, 517 | length: 2, 518 | stiffness: 0.9, 519 | render: { visible: false } 520 | })); 521 | } 522 | 523 | function createLoop(grp, stiffness, ignoreLast){ 524 | // ignoreLast: if true, ignore last body 525 | var bLength = grp.bodies.length; 526 | if(bLength == 1) return; 527 | for(var i = 0; i < bLength; i++){ 528 | if(ignoreLast && i == bLength - 1) break; 529 | var b = grp.bodies[i]; 530 | var nextIndex = i == bLength - 1 ? 0 : i + 1; 531 | var b1 = grp.bodies[nextIndex]; 532 | Composite.add(grp, Constraint.create({ 533 | bodyA: b, 534 | bodyB: b1, 535 | length: Vector.magnitude(Vector.sub(b.position, b1.position)), 536 | stiffness: stiffness, 537 | //render: { type: 'line' } 538 | render: { visible:false } 539 | })) 540 | } 541 | } 542 | 543 | function sortBodiesByNearest(bodies, farthest){ 544 | // if farthest == true, sorts by farthest 545 | if(bodies.length < 3) return bodies; 546 | var bs = [bodies[0]]; 547 | bodies.splice(0, 1); 548 | while(bodies.length > 0){ 549 | var b = bs[bs.length - 1]; 550 | var min_dist = -1, nearest_idx; 551 | for(var i = 0, iEnd = bodies.length; i < iEnd; i++){ 552 | var d = Vector.magnitudeSquared(Vector.sub(b.position, bodies[i].position)); 553 | if(farthest){ 554 | if(min_dist < 0 || d > min_dist){ 555 | min_dist = d; 556 | nearest_idx = i; 557 | } 558 | } else { 559 | if(min_dist < 0 || d < min_dist){ 560 | min_dist = d; 561 | nearest_idx = i; 562 | } 563 | } 564 | } 565 | bs.push(bodies[nearest_idx]); 566 | bodies.splice(nearest_idx, 1); 567 | } 568 | return bs; 569 | } 570 | 571 | function isCircle(item){ 572 | var b = item.bounds; 573 | var boundsRatio = calcRatio(item.bounds.width, item.bounds.height); 574 | var perimeterRatio = calcRatio(item.length, item.bounds.width * Math.PI); 575 | var areaRatio = calcRatio(item.area, Math.pow(item.bounds.width / 2, 2) * Math.PI); 576 | 577 | return boundsRatio < _circleRatioLimit.bounds 578 | && perimeterRatio < _circleRatioLimit.perimeter 579 | && areaRatio < _circleRatioLimit.area; 580 | } 581 | 582 | function calcRatio(a,b){ 583 | var v1, v2; 584 | if(a > b){ 585 | v1 = a; v2 = b; 586 | } else { 587 | v1 = b; v2 = a; 588 | } 589 | if(v2 == 0) return Infinity; 590 | return v1/v2; 591 | } 592 | 593 | function createCircle(item){ 594 | var b = item.bounds; 595 | return Bodies.circle(b.center.x, b.center.y, b.width / 2, getStyle(item)); 596 | } 597 | 598 | function createRectangle(item){ 599 | var b = item.bounds; 600 | var s = item.size; 601 | var body = Bodies.rectangle(b.center.x, b.center.y, s.width, s.height, getStyle(item)); 602 | if(item.rotation){ 603 | var angle = item.rotation * Math.PI / 180; 604 | Body.rotate(body, angle); 605 | } 606 | return body; 607 | } 608 | 609 | function createPolygon(c){ 610 | var center = c.bounds.center; 611 | var c1 = c.clone(); 612 | c1.flatten(FLATTEN_FLATNESS); 613 | var vertices = getPoints(c1, center); 614 | c1.remove(); 615 | removeDuplicatePoints(vertices); 616 | 617 | var options = getStyle(c); 618 | var body; 619 | if(!Vertices.isConvex(vertices)){ 620 | var parts = []; 621 | var decomped_parts = []; 622 | decompPolygon(vertices, decomped_parts); 623 | if(decomped_parts.length > 0){ 624 | for (var i = 0, iEnd = decomped_parts.length; i < iEnd; i++) { 625 | var part_body = Body.create(Common.extend(decomped_parts[i], options)); 626 | parts.push(part_body); 627 | } 628 | body = Body.create(options); 629 | Body.setParts(body, parts); 630 | Body.translate(body, center); 631 | } 632 | } else { 633 | body = Bodies.fromVertices(center.x, center.y, vertices, options); 634 | } 635 | return body; 636 | } 637 | 638 | function createCompoundBody(c, coll_group){ 639 | var center = c.bounds.center; 640 | var parts = []; 641 | var parts_blank = []; 642 | var options = { 643 | density : BODY_DENSITY, 644 | frictionAir : _spec.frictionAir, 645 | restitution : RESTITUTION_DEFAULT, 646 | isStatic : isFilledBlack(c), 647 | collisionFilter: { group: coll_group }, 648 | render : { fillStyle: CANVAS_BACKGROUND_COLOR } 649 | }; 650 | var c1 = c.clone(); 651 | c1.flatten(FLATTEN_FLATNESS); 652 | for(var ci = 0, ciEnd = c.children.length; ci < ciEnd; ci++){ 653 | var child = c1.children[ci]; 654 | var parts_tmp, part_label; 655 | var n = countContainsEx(ci, c.children); 656 | if(n % 2 == 0){ 657 | options.render.fillStyle = color2css(c.fillColor, CANVAS_BACKGROUND_COLOR); 658 | parts_tmp = parts; 659 | part_label = ""; 660 | } else { 661 | options.render.fillStyle = CANVAS_BACKGROUND_COLOR; 662 | parts_tmp = parts_blank; 663 | part_label = "blank"; 664 | } 665 | var vertices = getPoints(child, center); 666 | removeDuplicatePoints(vertices); 667 | 668 | if(!Vertices.isConvex(vertices)){ 669 | var decomped_parts = []; 670 | decompPolygon(vertices, decomped_parts); 671 | for (var i = 0, iEnd = decomped_parts.length; i < iEnd; i++) { 672 | var body = Body.create(Common.extend(decomped_parts[i], options)); 673 | body.label = part_label; 674 | parts_tmp.push(body); 675 | } 676 | } else { 677 | var body = Body.create(Common.extend({ 678 | position: Vertices.centre(vertices), 679 | vertices: vertices 680 | }, options)); 681 | body.label = part_label; 682 | parts_tmp.push(body); 683 | } 684 | } 685 | c1.remove(); 686 | 687 | var parts_all = parts.concat(parts_blank); 688 | var compound; 689 | if(parts_all.length > 0){ 690 | compound = Body.create({ 691 | density : BODY_DENSITY, 692 | frictionAir : _spec.frictionAir, 693 | restitution : RESTITUTION_DEFAULT, 694 | isStatic : isFilledBlack(c) }); 695 | Body.setParts(compound, parts_all); 696 | Body.translate(compound, center); 697 | //Body.setPosition(compound, center); 698 | } 699 | return compound; 700 | } 701 | 702 | // decomp processing referred to Bodies.fromVertices in matter.js 703 | function decompPolygon(vertices, decomped_parts){ 704 | if(decomp && decomp.quickDecomp){ 705 | var concave = vertices.map(function(vertex) { 706 | return [vertex.x, vertex.y]; 707 | }); 708 | decomp.makeCCW(concave); 709 | decomp.removeCollinearPoints(concave, REMOVE_COLLINEAR_PRECISION); 710 | var decomposed = decomp.quickDecomp(concave); 711 | 712 | if(decomposed.length < 1){ 713 | console.log("decomp failed"); 714 | _spec._decomp_fail_count++; 715 | return; 716 | } 717 | 718 | for (var i = 0, iEnd = decomposed.length; i < iEnd; i++) { 719 | var chunk = decomposed[i]; 720 | 721 | // convert vertices into the correct structure 722 | var chunkVertices = chunk.map(function(vertices) { 723 | return { 724 | x: vertices[0], 725 | y: vertices[1] 726 | }; 727 | }); 728 | 729 | // skip small chunks 730 | if (DECOMP_MINIMUM_AREA > 0 && Vertices.area(chunkVertices) < DECOMP_MINIMUM_AREA) 731 | continue; 732 | 733 | // create a compound part 734 | decomped_parts.push({ 735 | position: Vertices.centre(chunkVertices), 736 | vertices: chunkVertices 737 | }); 738 | } 739 | } 740 | } 741 | 742 | function countContainsEx(ci, children){ 743 | var n0 = countContains(ci, children, 0); 744 | var n1 = countContains(ci, children, parseInt(children[ci].segments.length / 2)); 745 | return Math.min(n0, n1); 746 | } 747 | 748 | function countContains(ci, children, idx){ 749 | // * A simple check to see how many shapes that include 750 | // children[ci] inside the elements (children) of compound path. 751 | // Assume that there are no intersections of elements. 752 | // If the result is an odd number, consider children[ci] to be a hole. 753 | // compound path の要素(children)の中に、 754 | // children[ci] を内側に含む図形がいくつあるか 755 | // の簡易的なチェック。要素の交差はないと仮定。 756 | // 結果が奇数の場合、children[ci] は hole と考える。 757 | var n = 0; 758 | var p = children[ci].segments[idx].point; 759 | for(var i = 0, iEnd = children.length; i < iEnd; i++){ 760 | if(i == ci) continue; 761 | if(children[i].contains(p)){ 762 | n++; 763 | } 764 | } 765 | return n; 766 | } 767 | 768 | function removeDuplicatePoints(points){ 769 | function checkDuplicatePoint(p, p1){ 770 | return Math.abs(p.x - p1.x) < DUPLICATE_POINT_TOLERANCE 771 | && Math.abs(p.y - p1.y) < DUPLICATE_POINT_TOLERANCE; 772 | } 773 | var idx = points.length - 1; 774 | var p = points[idx]; 775 | var p1; 776 | while(idx > 0){ 777 | idx--; 778 | p1 = points[idx]; 779 | if(checkDuplicatePoint(p, p1)){ 780 | points.splice(idx, 1); 781 | } else { 782 | p = p1; 783 | } 784 | } 785 | if(points.length > 1){ 786 | p = points[points.length - 1]; 787 | p1 = points[0]; 788 | if(checkDuplicatePoint(p, p1)){ 789 | points.splice(0, 1); 790 | } 791 | } 792 | } 793 | 794 | function isFilledBlack(item){ 795 | var result = false; 796 | var fc = item.fillColor; 797 | //return (fc && fc.red == 0 && fc.green == 0 && fc.blue == 0); 798 | var rgb = AS_BLACK_THRESHOLD_RGB; 799 | if(fc && fc.red <= rgb[0] && fc.green <= rgb[1] && fc.blue <= rgb[2]){ 800 | fc.red = 0; fc.green = 0; fc.blue = 0; 801 | result = true; 802 | } 803 | return result; 804 | } 805 | 806 | function color2css(col, nullValue){ 807 | return col ? col.toCSS() : nullValue; 808 | } 809 | 810 | function getStyle(item){ 811 | // ・塗り=null にするとデフォルト色(ランダムカラー)が割り当てられる模様 812 | //  なので、塗りなしの場合、背景色を設定している。 813 | return { 814 | density : BODY_DENSITY, 815 | frictionAir : _spec.frictionAir, 816 | restitution : RESTITUTION_DEFAULT, 817 | isStatic : isFilledBlack(item), 818 | render : { 819 | fillStyle: color2css(item.fillColor, CANVAS_BACKGROUND_COLOR), 820 | strokeStyle : color2css(item.strokeColor, null), 821 | lineWidth : item.strokeColor ? item.strokeWidth : 0, 822 | opacity : item.opacity 823 | } 824 | } 825 | } 826 | 827 | function getPoints(item, center){ 828 | var segs = item.segments; 829 | var r = []; 830 | for(var i = 0, iEnd = segs.length; i < iEnd; i++){ 831 | r.push(segs[i].point.subtract(center)); 832 | } 833 | return r; 834 | } 835 | 836 | /* function getCentroid(item){ 837 | var segs = item.segments; 838 | var p = segs[0].point.clone(); 839 | for(var i = 1, iEnd = segs.length; i < iEnd; i++){ 840 | p.x += segs[i].point.x; 841 | p.y += segs[i].point.y; 842 | } 843 | return p.multiply(1 / segs.length); 844 | } */ 845 | 846 | // ---------------------- 847 | // functions to output 848 | // ---------------------- 849 | // body.id に関連づけた、paper.js でimportした元の形状を、 850 | // 別レイヤーにコピーし、body の移動・回転を適用したうえで exportSVG する。 851 | function exportSVGtoFile(){ 852 | if(confirm(getMessage("export_svg_confirm"))){ 853 | try{ 854 | var active_layer = paper.project.activeLayer; 855 | var layer = body2path(); 856 | active_layer.visible = false; 857 | 858 | if(_spec.url) URL.revokeObjectURL(_spec.url); 859 | var svg = paper.project.exportSVG(); 860 | var seri = new XMLSerializer(); 861 | var s = seri.serializeToString(svg); 862 | _spec.url = handleDownload(s); 863 | alert(getMessage("exported")); 864 | } catch(e){ 865 | console.log(e); 866 | alert(getMessage("failed_to_export")); 867 | } finally { 868 | active_layer.activate(); 869 | active_layer.visible = true; 870 | layer.remove(); 871 | } 872 | } 873 | } 874 | 875 | function body2path(){ 876 | var all_bodies = Composite.allBodies(_engine.world); 877 | var layer = new paper.Layer(); 878 | layer.activate(); 879 | for(var i = 0, iEnd = all_bodies.length; i < iEnd ; i++){ 880 | var body = all_bodies[i]; 881 | if(!(body.id in _svgShapes)) continue; 882 | 883 | // クライアント枠の外側にあるものは出力しない 884 | if(body.bounds.max.y < 0 || body.bounds.min.y > window.innerHeight 885 | || body.bounds.max.x < 0 || body.bounds.min.x > window.innerWidth){ 886 | continue; 887 | } 888 | 889 | var path = _svgShapes[body.id].copyTo(layer); 890 | if(body.label != "Circle Body"){ 891 | path.rotate(body.angle * 180 / Math.PI); 892 | } 893 | var body_center = new paper.Point( 894 | (body.bounds.max.x + body.bounds.min.x) / 2, 895 | (body.bounds.max.y + body.bounds.min.y) / 2 896 | ); 897 | path.translate(body_center.subtract(path.position)); 898 | } 899 | 900 | // export hang constraints 901 | var all_constraints = Composite.allConstraints(_engine.world); 902 | for(var i = 0, iEnd = all_constraints.length; i < iEnd ; i++){ 903 | var constraint = all_constraints[i]; 904 | if(constraint.label == "hang"){ 905 | var path = new paper.Path.Line( 906 | Constraint.pointAWorld(constraint), 907 | Constraint.pointBWorld(constraint)); 908 | path.strokeColor = constraint.render.strokeStyle; 909 | path.strokeWidth = constraint.render.strokeWidth; 910 | } 911 | } 912 | return layer; 913 | } 914 | 915 | function handleDownload(text) { 916 | var blob = new Blob([ text ], { "type" : "image/svg+xml" }); 917 | var a = document.createElement("a"); 918 | a.target = '_blank'; 919 | a.download = EXPORT_SVG_FILENAME; 920 | var url; 921 | var winURL = window.URL || window.webkitURL; 922 | if(winURL && winURL.createObject) { //chrome 923 | url = winURL.createObjectURL(blob); 924 | a.href = url; 925 | a.click(); 926 | } else if(window.URL && window.URL.createObjectURL) { //firefox 927 | url = window.URL.createObjectURL(blob); 928 | a.href = url; 929 | document.body.appendChild(a); 930 | a.click(); 931 | document.body.removeChild(a); 932 | } 933 | return url; 934 | } 935 | 936 | // ---------------------- 937 | // initialize 938 | // ---------------------- 939 | // HTML上のステータステキストを更新する 940 | function updateStatus(){ 941 | var lang = getLang(); 942 | 943 | $("#stat_gravity_" + lang).text( 944 | getMessage("stat_gravity") + (_spec.gravity_y ? GRAVITY_Y_DEFAULT : 0)); 945 | $("#stat_gravity_" + lang).css("color", _spec.gravity_y ? "#888" : "red"); 946 | 947 | $("#stat_frictionAir_" + lang).text( 948 | getMessage("stat_frictionAir") 949 | + (_spec.frictionAir == FRICTIONAIR_DEFAULT 950 | ? getMessage("stat_normal") 951 | : getMessage("stat_frictionAir_high"))); 952 | $("#stat_frictionAir_" + lang).css( 953 | "color", 954 | _spec.frictionAir == FRICTIONAIR_DEFAULT ? "#888" : "red"); 955 | 956 | $("#stat_display_" + lang).text( 957 | getMessage("stat_display") 958 | + (_spec.wireframes 959 | ? getMessage("stat_wireframes") 960 | : getMessage("stat_normal"))); 961 | } 962 | 963 | function beforeApplyKeyCommand(){ 964 | // キーイベントによるコマンド実行前の処理。 965 | // 動いている場合はポーズする 966 | var isRunning = !_spec.isPause; 967 | if(_runner && !_spec.isPause){ 968 | Runner.stop(_runner); 969 | _spec.isPause = true; 970 | } 971 | return isRunning; 972 | } 973 | 974 | function afterApplyKeyCommand(isRunning){ 975 | // キーイベントによるコマンド実行後の処理。 976 | // 実行前に動いていた場合は、また動かす 977 | if(isRunning && _runner && _spec.isPause){ 978 | Runner.run(_runner, _engine); 979 | _spec.isPause = false; 980 | } 981 | } 982 | 983 | function key_exportSVGtoFile(){ 984 | if(!_engine){ 985 | alert(getMessage("no_data")); 986 | return; 987 | } 988 | var isRunning = beforeApplyKeyCommand(); 989 | exportSVGtoFile(); 990 | afterApplyKeyCommand(isRunning); 991 | } 992 | 993 | function key_wireFrame(){ 994 | // 読み込み前に設定できるようにしている 995 | _spec.wireframes = !_spec.wireframes 996 | if(_render){ 997 | _render.options.wireframes = _spec.wireframes; 998 | } 999 | updateStatus(); 1000 | } 1001 | 1002 | function key_pause(){ 1003 | if(_runner){ 1004 | if(_spec.isPause){ 1005 | Runner.run(_runner, _engine); 1006 | } else { 1007 | Runner.stop(_runner); 1008 | } 1009 | _spec.isPause = !_spec.isPause; 1010 | } 1011 | } 1012 | 1013 | function key_gravity(){ 1014 | // 読み込み前に設定できるようにしている 1015 | _spec.gravity_y = _spec.gravity_y ? 0 : GRAVITY_Y_DEFAULT; 1016 | if(_engine){ 1017 | _engine.world.gravity.y = _spec.gravity_y; 1018 | } 1019 | updateStatus(); 1020 | } 1021 | 1022 | function key_frictionAir(){ 1023 | // 読み込み前に設定できるようにしている 1024 | _spec.frictionAir = _spec.frictionAir == FRICTIONAIR_DEFAULT ? FRICTIONAIR_HIGH : FRICTIONAIR_DEFAULT; 1025 | if(_engine){ 1026 | var isRunning = beforeApplyKeyCommand(); 1027 | 1028 | var bodies = Composite.allBodies(_engine.world); 1029 | for(var i = 0; i < bodies.length; i++){ 1030 | var body = bodies[i]; 1031 | if( body.isStatic) continue; 1032 | body.frictionAir = _spec.frictionAir; 1033 | } 1034 | 1035 | afterApplyKeyCommand(isRunning); 1036 | } 1037 | updateStatus(); 1038 | } 1039 | 1040 | function init() { 1041 | showHelpByLocale(); 1042 | 1043 | document.addEventListener('dragover', handleDragOver); 1044 | document.addEventListener('drop', handleFileSelect); 1045 | 1046 | document.addEventListener('keydown',(evt) => { 1047 | const keyName = evt.key.toLowerCase(); 1048 | if(keyName == "s"){ 1049 | key_exportSVGtoFile(); 1050 | } else if(keyName == "v"){ 1051 | importSVGFromClipboard_paper(); 1052 | } else if(keyName == "w"){ 1053 | key_wireFrame(); 1054 | } else if(keyName == "p"){ 1055 | key_pause(); 1056 | } else if(keyName == "g"){ 1057 | key_gravity(); 1058 | } else if(keyName == "a"){ 1059 | key_frictionAir(); 1060 | } 1061 | }); 1062 | } 1063 | 1064 | function showHelpByLocale(){ 1065 | if(getLang() == "ja"){ 1066 | $("#help_ja").show(); 1067 | $("#status_ja").show(); 1068 | } else { 1069 | $("#help_en").show(); 1070 | $("#status_en").show(); 1071 | } 1072 | } 1073 | 1074 | // ---------------------- 1075 | // locale 1076 | // ---------------------- 1077 | function getLang(){ 1078 | //return "en"; // for test 1079 | return navigator.language.startsWith("ja") ? "ja" : "en"; 1080 | } 1081 | 1082 | function getMessage(entry){ 1083 | var obj = _messages[entry]; 1084 | var lang = getLang(); 1085 | 1086 | if(obj){ 1087 | return obj[lang] || obj["en"].toString(); 1088 | } else { 1089 | return "undefined:[" + entry + "]"; 1090 | } 1091 | } 1092 | 1093 | _messages = { 1094 | already_loaded : { 1095 | "en" : "already loaded. please reload the page to refresh", 1096 | "ja" : "すでに読み込まれたデータがあります。別のデータを読み込むには頁をリロードしてください。" 1097 | }, 1098 | please_select_a_svg : { 1099 | "en" : "Please drop a SVG file.(Dropped file is ", 1100 | "ja" : "SVGファイルをドロップしてください。(入力ファイル=" 1101 | }, 1102 | paste_failed : { 1103 | "en" : "paste failed", 1104 | "ja" : "貼付データの読み込みに失敗しました" 1105 | }, 1106 | paste_chrome_only_for_now : { 1107 | "en" : "Loading by paste is currently available only for Chrome(ver.66 or later)", 1108 | "ja" : "貼付によるロードは今のところChrome(ver.66以降)のみ対応です" 1109 | }, 1110 | no_data : { 1111 | "en" : "no data", 1112 | "ja" : "出力データなし" 1113 | }, 1114 | export_svg_confirm : { 1115 | "en" : "export SVG file?", 1116 | "ja" : "SVGファイルをエクスポートしますか?" 1117 | }, 1118 | exported : { 1119 | "en" : "exported", 1120 | "ja" : "エクスポート完了" 1121 | }, 1122 | failed_to_export : { 1123 | "en" : "failed to export", 1124 | "ja" : "エクスポートに失敗しました" 1125 | }, 1126 | stat_gravity : { 1127 | "en" : "gravity=", 1128 | "ja" : "重力=" 1129 | }, 1130 | stat_frictionAir : { 1131 | "en" : "airResistance=", 1132 | "ja" : "空気抵抗=" 1133 | }, 1134 | stat_normal : { 1135 | "en" : "normal", 1136 | "ja" : "通常" 1137 | }, 1138 | stat_frictionAir_high : { 1139 | "en" : "HIGH", 1140 | "ja" : "重い" 1141 | }, 1142 | stat_display : { 1143 | "en" : "display=", 1144 | "ja" : "表示=" 1145 | }, 1146 | stat_wireframes : { 1147 | "en" : "wireframes", 1148 | "ja" : "ワイヤーフレーム" 1149 | } 1150 | } 1151 | 1152 | init(); 1153 | }()); 1154 | 1155 | -------------------------------------------------------------------------------- /js/lib/jquery-3.7.1.slim.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||C,T)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=C&&9===n.nodeType&&n.documentElement&&(r=(C=n).documentElement,T=!ce.isXMLDoc(C),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=C&&(t=C.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!C.getElementsByName||!C.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return C.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&T)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=C.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===C||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),C}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),T&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),C.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Te={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=C.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0