mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-01-01 02:38:43 -05:00
Add VoronoiTool to examples (work in progress).
This commit is contained in:
parent
b643fdfc1f
commit
1bb9a262d9
1 changed files with 627 additions and 0 deletions
627
examples/Tools/VoronoiTool.html
Normal file
627
examples/Tools/VoronoiTool.html
Normal file
|
@ -0,0 +1,627 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Example</title>
|
||||
<script type="text/javascript">var root = '../../'</script>
|
||||
<script type="text/javascript" src="../../src/load.js"></script>
|
||||
<script type="text/javascript" src="http://www.raymondhill.net/voronoi/rhill-voronoi-core-min.js"></script>
|
||||
|
||||
<script type="text/paperscript" canvas="canvas">
|
||||
document.currentStyle = {
|
||||
strokeWidth: 1,
|
||||
}
|
||||
var shiftDown, altDown;
|
||||
function onMouseDown(event) {
|
||||
// shiftDown = Key.isDown('shift');
|
||||
// altDown = Key.isDown('alt');
|
||||
|
||||
voronoiDrawer.initialize();
|
||||
if(altDown || shiftDown)
|
||||
var removed = voronoiDrawer.removePoint(event.point);
|
||||
if(altDown) {
|
||||
if(removed) {
|
||||
voronoiDrawer.initPoints();
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
} else {
|
||||
voronoiDrawer.addAndRender(event.point, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDrag(event) {
|
||||
if(!altDown) {
|
||||
if(event.count == 1)
|
||||
voronoiDrawer.showPointsDown();
|
||||
voronoiDrawer.addAndRender(event.point, true, true)
|
||||
voronoiDrawer.showPointsDrag();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
if(!altDown && !shiftDown) {
|
||||
voronoiDrawer.addAndRender(event.point, false, false)
|
||||
}
|
||||
if(shiftDown) {
|
||||
voronoiDrawer.initPoints();
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
}
|
||||
|
||||
var voronoiDrawer = {
|
||||
voronoi: new Voronoi(),
|
||||
points: [],
|
||||
|
||||
initialize: function() {
|
||||
this.getGroup();
|
||||
this.setBbox();
|
||||
},
|
||||
|
||||
getGroup: function() {
|
||||
// this.group = null;
|
||||
// this.vGroup = null;
|
||||
// var groups = document.getItems({
|
||||
// type: Group
|
||||
// });
|
||||
// for(var i = 0, l = groups.length; i < l; i++) {
|
||||
// var group = groups[i];
|
||||
// if(group.name == 'Voronoi') {
|
||||
// this.group = group;
|
||||
// this.vGroup = group.children['vGroup'];
|
||||
// i = l;
|
||||
// }
|
||||
// }
|
||||
if(!this.group) {
|
||||
this.group = new Group();
|
||||
this.group.name = 'Voronoi';
|
||||
this.group.data = {};
|
||||
var path = new Path();
|
||||
this.group.data.path = path;
|
||||
path.removeFromParent();
|
||||
}
|
||||
return this.group;
|
||||
},
|
||||
|
||||
initPoints: function() {
|
||||
this.points = [];
|
||||
var segments = this.group.data.path.segments;
|
||||
for(var i = 0, l = segments.length; i < l; i++) {
|
||||
this.points.push(segments[i].point);
|
||||
}
|
||||
},
|
||||
|
||||
setBbox: function() {
|
||||
this.bbox = {
|
||||
xl: 0,
|
||||
xr: document.bounds.width,
|
||||
yt: 0,
|
||||
yb: document.bounds.height
|
||||
}
|
||||
},
|
||||
|
||||
addPoint: function(point) {
|
||||
this.group.data.path.add(point.round());
|
||||
},
|
||||
|
||||
setPoints: function(points) {
|
||||
for(var i = 0, l = points.length; i < l; i++) {
|
||||
points[i] = points[i].round();
|
||||
}
|
||||
this.points = points;
|
||||
this.group.data.path = new Path(points);
|
||||
},
|
||||
|
||||
addRandomPoint: function() {
|
||||
var point = Point.random() * document.bounds.size;
|
||||
this.addPoint(point);
|
||||
},
|
||||
|
||||
pop: function() {
|
||||
this.group.data.path.segments.pop();
|
||||
},
|
||||
|
||||
showPointsDown: function() {
|
||||
var size = 2 / document.activeView.zoom;
|
||||
for(var i = 0, l = this.points.length - 1; i < l; i++) {
|
||||
new Path.Circle(this.points[i], size).removeOn({
|
||||
up: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
showPointsDrag: function() {
|
||||
var size = 2 / document.activeView.zoom;
|
||||
new Path.Circle(this.points[this.points.length - 1], size).removeOn({
|
||||
up: true,
|
||||
drag: true
|
||||
});
|
||||
},
|
||||
|
||||
getOffsettedPath: function(path) {
|
||||
var cloned = path.clone();
|
||||
var offset = Math.min(5, Math.max(Math.abs(path.area) / 4000, 1));
|
||||
path.fillColor = null;
|
||||
path.strokeColor='black';
|
||||
path.strokeWidth = offset * 2;
|
||||
var offsetted = path.expand();
|
||||
if(offsetted && offsetted.children.length) {
|
||||
var compoundPath = offsetted.children[0];
|
||||
if(compoundPath.children.length) {
|
||||
var inner = offsetted.children[0].children[0];
|
||||
inner.moveAbove(offsetted);
|
||||
inner.fillColor = 'black';
|
||||
}
|
||||
offsetted.remove();
|
||||
}
|
||||
if(inner)
|
||||
inner.data.path = cloned;
|
||||
return inner;
|
||||
},
|
||||
|
||||
removeSmallBits: function(path) {
|
||||
var averageLength = path.length / path.segments.length;
|
||||
for(var i = 0, l = path.segments.length; i < l; i++) {
|
||||
var segment = path.segments[i];
|
||||
var nextSegment = path.segments[i + 1 >= l ? (i + 1) % l : i + 1];
|
||||
var length = (nextSegment.point - segment.point).length;
|
||||
if(length / averageLength < 0.1) {
|
||||
var prevSegment = path.segments[i - 1 < 0 ? l - 1 : i - 1];
|
||||
var line1 = new Line(prevSegment.point, segment.point, true);
|
||||
var afterSegment = path.segments[i + 2 >= l ? (i + 2) % l : i + 2];
|
||||
var line2 = new Line(nextSegment.point, afterSegment.point, true);
|
||||
var intersection = line1.getIntersectionPoint(line2);
|
||||
path.insert(i + 1 >= l ? (i + 1) % l : i + 1, intersection);
|
||||
segment.remove();
|
||||
nextSegment.remove();
|
||||
l = path.segments.length;
|
||||
i = 0;
|
||||
averageLength = path.length / path.segments.length;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
roundPath: function(path) {
|
||||
var segments = path.segments.clone();
|
||||
path.segments = [];
|
||||
for(var i = 0, l = segments.length; i < l; i++) {
|
||||
var curPoint = segments[i].point;
|
||||
var nextPoint = segments[i + 1 == l ? 0 : i + 1].point;
|
||||
var prevPoint = segments[i - 1 < 0 ? segments.length - 1 : i - 1].point;
|
||||
var nextDelta = curPoint - nextPoint;
|
||||
var prevDelta = curPoint - prevPoint;
|
||||
nextDelta.length *= 0.2;
|
||||
prevDelta.length *= 0.2;
|
||||
path.add(curPoint - prevDelta);
|
||||
path.add(curPoint - nextDelta);
|
||||
}
|
||||
},
|
||||
|
||||
redrawVoronoi: function(diagram, activePoint) {
|
||||
for(var i = 0, l = diagram.sites.length; i < l; i++) {
|
||||
this.drawCell(diagram, i);
|
||||
}
|
||||
},
|
||||
|
||||
drawVoronoi: function(diagram, activePoint) {
|
||||
|
||||
if(this.points.length == 2)
|
||||
this.drawCell(diagram, 0, activePoint);
|
||||
|
||||
var newCell = this.drawCell(diagram, this.points.length - 1, activePoint);
|
||||
if(!activePoint && newCell) {
|
||||
var checkPath = newCell.data.path;
|
||||
for(var i = 0, l = this.vGroup.children.length; i < l; i++) {
|
||||
var path = this.vGroup.children[i];
|
||||
var toCheckPath = path.data.path;
|
||||
if(toCheckPath && checkPath.bounds.intersects(toCheckPath.bounds)) {// && toCheckPath.intersects(checkPath)) {
|
||||
var index = this.findIndex(toCheckPath.data.center);
|
||||
path.remove();
|
||||
this.drawCell(diagram, index, activePoint);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
removeAllPoints: function() {
|
||||
this.group.data.path.segments = [];
|
||||
},
|
||||
|
||||
removePoint: function(point) {
|
||||
|
||||
var hitResult = this.vGroup.hitTest(point);
|
||||
if(hitResult) {
|
||||
var point = hitResult.item.data.path.data.center;
|
||||
var segments = this.group.data.path.segments;
|
||||
for(var i = 0, l = segments.length; i < l; i++) {
|
||||
var segment = segments[i];
|
||||
if(point == segment.point.multiply(100).round()) {
|
||||
i = l;
|
||||
segment.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return !!hitResult;
|
||||
},
|
||||
|
||||
findIndex: function(point) {
|
||||
for(var i = 0, l = this.points.length; i < l; i++) {
|
||||
if(point == this.points[i].multiply(100).round())
|
||||
return i;
|
||||
}
|
||||
},
|
||||
|
||||
drawCell: function(diagram, index, remove) {
|
||||
var cell = diagram.cells[diagram.sites[index].id];
|
||||
// there is no guarantee a Voronoi cell will exist for any
|
||||
// particular site
|
||||
if (cell !== undefined) {
|
||||
var halfedges = cell.halfedges;
|
||||
var nHalfedges = halfedges.length;
|
||||
if (nHalfedges < 3) return;
|
||||
var path = new Path();
|
||||
//path.strokeColor = 'black';
|
||||
path.data = {};
|
||||
if(remove) {
|
||||
path.removeOn({
|
||||
drag: true,
|
||||
up: true
|
||||
});
|
||||
}
|
||||
if(!remove)
|
||||
this.vGroup.appendTop(path);
|
||||
|
||||
var startPoint = new Point(halfedges[0].getStartpoint());
|
||||
path.add(startPoint);
|
||||
for (var iHalfedge = 0; iHalfedge < nHalfedges; iHalfedge++) {
|
||||
var curPoint = new Point(halfedges[iHalfedge].getEndpoint());
|
||||
path.add(curPoint);
|
||||
}
|
||||
path.data.center = this.points[index].multiply(100).round();
|
||||
path.closed = true;
|
||||
path.fillColor = new GrayColor(1 - path.length / 4000);
|
||||
|
||||
if(values.drawBlobs) {
|
||||
var offPath = this.getOffsettedPath(path);
|
||||
if(offPath) {
|
||||
this.removeSmallBits(offPath);
|
||||
this.roundPath(offPath);
|
||||
|
||||
if(values.smoothBlobs) {
|
||||
offPath.smooth();
|
||||
}
|
||||
if(!remove) {
|
||||
this.vGroup.appendTop(offPath);
|
||||
} else {
|
||||
offPath.removeOn({
|
||||
drag: true,
|
||||
up: true
|
||||
});
|
||||
}
|
||||
}
|
||||
path = offPath;
|
||||
} else {
|
||||
var dataPath = new Path(path.segments);
|
||||
dataPath.data = {};
|
||||
dataPath.data.center = path.data.center;
|
||||
path.data.path = dataPath;
|
||||
dataPath.remove();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
},
|
||||
|
||||
fastRender: function(activePoint) {
|
||||
if(!this.vGroup) {
|
||||
this.vGroup = new Group();
|
||||
this.vGroup.name = 'vGroup';
|
||||
this.group.appendTop(this.vGroup);
|
||||
}
|
||||
this.voronoi.setSites(this.points);
|
||||
var diagram = this.voronoi.compute(this.bbox);
|
||||
if(this.points.length > 1)
|
||||
this.drawVoronoi(diagram, activePoint);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if(this.vGroup)
|
||||
this.vGroup.remove();
|
||||
this.vGroup = new Group();
|
||||
this.vGroup.name = 'vGroup';
|
||||
this.group.appendTop(this.vGroup);
|
||||
this.voronoi.setSites(this.points);
|
||||
var diagram = this.voronoi.compute(this.bbox);
|
||||
this.redrawVoronoi(diagram);
|
||||
},
|
||||
|
||||
addAndRender: function(point, preview, remove) {
|
||||
|
||||
if(remove) this.pop();
|
||||
|
||||
this.addPoint(point);
|
||||
this.initPoints();
|
||||
if(preview) {
|
||||
this.render(point);
|
||||
} else {
|
||||
this.render(null, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var values = {
|
||||
randomAmount: 10,
|
||||
drawBlobs: false,
|
||||
smoothBlobs: true
|
||||
}
|
||||
|
||||
var drawers = {
|
||||
// 'Hexagons': function() {
|
||||
// var components = {
|
||||
// columns: { type: 'number', label: 'Columns', value: this.columns || 5 },
|
||||
// rows: { type: 'number', label: 'Rows', value: this.rows || 8 }
|
||||
// };
|
||||
// var vPoints = [];
|
||||
// // Now we bring up the dialog
|
||||
// var values = Dialog.prompt('Settings', components);
|
||||
// if(values) {
|
||||
// this.columns = values.columns;
|
||||
// this.rows = values.rows * 2;
|
||||
//
|
||||
// var size = new Size(this.columns, this.rows);
|
||||
// var col = document.bounds.size / size;
|
||||
// for(var i = -1; i < this.columns + 1; i++) {
|
||||
// for(var j = -1; j < this.rows + 1; j++) {
|
||||
// var point = new Point(i, j) / new Point(size) * document.bounds.size + col / 2;
|
||||
// if(j % 4 == 2 || j% 4 == 3) {
|
||||
// point += new Point(col.width / 2, 0);
|
||||
// }
|
||||
//
|
||||
// vPoints.push(point);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return vPoints;
|
||||
// },
|
||||
|
||||
'Bee Hive': function() {
|
||||
var components = {
|
||||
columns: { type: 'number', label: 'Columns', value: this.columns || 5 },
|
||||
rows: { type: 'number', label: 'Rows', value: this.rows || 8 },
|
||||
loose: { type: 'checkbox', label: 'Loose', value: true }
|
||||
};
|
||||
var vPoints = [];
|
||||
// Now we bring up the dialog
|
||||
var values = Dialog.prompt('Settings', components);
|
||||
if(values) {
|
||||
this.columns = values.columns;
|
||||
this.rows = values.rows;
|
||||
|
||||
var size = new Size(this.columns, this.rows);
|
||||
var col = document.bounds.size / size;
|
||||
for(var i = -1; i < this.columns + 1; i++) {
|
||||
for(var j = -1; j < this.rows + 1; j++) {
|
||||
var point = new Point(i, j) / new Point(size) * document.bounds.size + col / 2;
|
||||
if(j % 2)
|
||||
point += new Point(col.width / 2, 0);
|
||||
if(values.loose)
|
||||
point += (col / 4) * Point.random() - col / 4;
|
||||
vPoints.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return vPoints;
|
||||
},
|
||||
|
||||
'Teeth': function() {
|
||||
var components = {
|
||||
columns: { type: 'number', label: 'Columns', value: this.columns || 15 },
|
||||
rows: { type: 'number', label: 'Rows', value: this.rows || 15 },
|
||||
loose: { type: 'checkbox', label: 'Loose', value: true }
|
||||
};
|
||||
var vPoints = [];
|
||||
// Now we bring up the dialog
|
||||
var values = Dialog.prompt('Settings', components);
|
||||
if(values) {
|
||||
this.columns = values.columns;
|
||||
this.rows = values.rows;
|
||||
var size = new Size(this.columns, this.rows);
|
||||
var col = document.bounds.size / size;
|
||||
for(var i = -1; i < this.columns ; i++) {
|
||||
for(var j = -1; j < this.rows; j++) {
|
||||
|
||||
var point = new Point(i, j) / size * document.bounds.size + col / 2;
|
||||
if(j % 2)
|
||||
point += new Point(col.width / 2, 0);
|
||||
if(i % 2)
|
||||
point += new Point(0, col.height / 2);
|
||||
if(values.loose)
|
||||
point += (col / 8) * Point.random() - col / 8;
|
||||
vPoints.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return vPoints;
|
||||
},
|
||||
'Grid': function() {
|
||||
var components = {
|
||||
columns: { type: 'number', label: 'Columns', value: this.columns || 3 },
|
||||
rows: { type: 'number', label: 'Rows', value: this.rows || 5 },
|
||||
loose: { type: 'checkbox', label: 'Loose', value: Base.pick(this.loose, false) }
|
||||
};
|
||||
var vPoints = [];
|
||||
// Now we bring up the dialog
|
||||
var values = Dialog.prompt('Settings', components);
|
||||
if(values) {
|
||||
this.columns = values.columns;
|
||||
this.rows = values.rows;
|
||||
this.parts = values.parts;
|
||||
this.loose = values.loose;
|
||||
|
||||
var size = new Size(this.columns, this.rows);
|
||||
var col = document.bounds.size / size;
|
||||
for(var i = -1; i < this.columns + 1; i++) {
|
||||
for(var j = -1; j < this.rows + 1; j++) {
|
||||
var point = new Point(i, j) / new Point(size) * document.bounds.size + col / 2;
|
||||
if(this.loose)
|
||||
point += (col / 4) * Point.random() - col / 4;
|
||||
vPoints.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
return vPoints;
|
||||
},
|
||||
'Citrus': function() {
|
||||
var components = {
|
||||
columns: { type: 'number', label: 'Columns', value: this.columns || 3 },
|
||||
rows: { type: 'number', label: 'Rows', value: this.rows || 5 },
|
||||
parts: { type: 'number', label: 'Rows', value: this.parts || 8 }
|
||||
};
|
||||
var vPoints = [];
|
||||
// Now we bring up the dialog
|
||||
var values = Dialog.prompt('Settings', components);
|
||||
if(values) {
|
||||
this.columns = values.columns;
|
||||
this.rows = values.rows;
|
||||
this.parts = values.parts;
|
||||
|
||||
var size = new Size(this.columns, this.rows);
|
||||
var col = document.bounds.size / size;
|
||||
for(var i = -1; i < this.columns + 1; i++) {
|
||||
for(var j = -1; j < this.rows + 1; j++) {
|
||||
var point = new Point(i, j) / new Point(size) * document.bounds.size + col / 2;
|
||||
if(j % 2)
|
||||
point += new Point(col.width / 2, 0);
|
||||
vPoints.push(point);
|
||||
var offset = i % 2 ? 45 : 0;
|
||||
for(var z = 0; z < this.parts; z++) {
|
||||
var vector = new Point(col.width / 6, 0).rotate(offset + z / this.parts * 360 * (i % 2 ? 1 : -1) * (j % 2 ? -1 : 1));
|
||||
vPoints.push(point + vector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vPoints;
|
||||
},
|
||||
'Circular': function() {
|
||||
var point = document.bounds.center;
|
||||
var vPoints = [point];
|
||||
var segments = 10;
|
||||
var height = document.bounds.height * 1.5 / 10;
|
||||
var vector = new Point(10, 0).normalize(height / 4);
|
||||
var offset = 0;
|
||||
for (var i = 0; i < 10; i++) {
|
||||
segments += 2;
|
||||
offset += 20;
|
||||
for (var j = 0; j < segments; j++) {
|
||||
vector.angle = i % 2 ? 360 / segments / 2 : 0;
|
||||
vector.angle += 360 / segments * j + offset;
|
||||
vPoints.push(point + vector);
|
||||
}
|
||||
vector.length += height / 2;
|
||||
}
|
||||
return vPoints;
|
||||
}
|
||||
}
|
||||
|
||||
var options = [];
|
||||
Base.each(drawers, function(drawer, i) {
|
||||
options.push(i);
|
||||
});
|
||||
|
||||
values.drawer = options[0];
|
||||
|
||||
var components = {
|
||||
ruler0: { label: 'Selected Paths', type: 'ruler' },
|
||||
addPoints: {
|
||||
type: 'button',
|
||||
value: 'Add Selected Points',
|
||||
onClick: function() {
|
||||
var paths = document.getItems({
|
||||
type: Path,
|
||||
selected: true
|
||||
});
|
||||
|
||||
if(paths.length) {
|
||||
voronoiDrawer.initialize();
|
||||
for(var i = 0, l = paths.length; i < l; i++) {
|
||||
var segments = paths[i].segments;
|
||||
for(var j = 0, k = segments.length; j < k; j++) {
|
||||
voronoiDrawer.addPoint(segments[j].point);
|
||||
}
|
||||
}
|
||||
voronoiDrawer.initPoints();
|
||||
voronoiDrawer.render();
|
||||
} else {
|
||||
Dialog.alert('Please select a path first.');
|
||||
}
|
||||
}
|
||||
},
|
||||
ruler1: { label: 'Random Points', type: 'ruler' },
|
||||
randomAmount: {
|
||||
steppers: true,
|
||||
label: 'Amount'
|
||||
},
|
||||
addRandomPoints: {
|
||||
type: 'button',
|
||||
value: 'Add Random Points',
|
||||
onClick: function() {
|
||||
voronoiDrawer.initialize();
|
||||
for(var i = 0, l = values.randomAmount; i < l; i++) {
|
||||
voronoiDrawer.addRandomPoint();
|
||||
}
|
||||
voronoiDrawer.initPoints();
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
},
|
||||
ruler2: { label: 'Grid Generators', type: 'ruler' },
|
||||
drawer: {
|
||||
label: 'Type',
|
||||
options: options
|
||||
},
|
||||
renderGrid: {
|
||||
type: 'button',
|
||||
value: 'Render Grid',
|
||||
onClick: function() {
|
||||
voronoiDrawer.initialize();
|
||||
var points = drawers[values.drawer]();
|
||||
if(points.length) {
|
||||
var confirmed = true;
|
||||
if(points.length > 800) {
|
||||
confirmed = Dialog.confirm('You are about to generate a Voronoi diagram with ' + points.length + ' points. This could take a while and could potentially crash Illustrator. Do you want to live recklessly?');
|
||||
}
|
||||
if(confirmed) {
|
||||
voronoiDrawer.setPoints(points);
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ruler3: { label: 'Settings', type: 'ruler' },
|
||||
drawBlobs: {
|
||||
label: 'Inset Paths',
|
||||
onChange: function(value) {
|
||||
components.smoothBlobs.enabled = !!value;
|
||||
voronoiDrawer.initialize();
|
||||
voronoiDrawer.initPoints();
|
||||
if(voronoiDrawer.points.length)
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
},
|
||||
smoothBlobs: {
|
||||
label: 'Smooth',
|
||||
onChange: function(value) {
|
||||
voronoiDrawer.initialize();
|
||||
voronoiDrawer.initPoints();
|
||||
if(voronoiDrawer.points.length)
|
||||
voronoiDrawer.render();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// var palette = new Palette('Voronoi Tool', components, values);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id='canvas' width=1024 height=768></canvas>
|
||||
</body>
|
Loading…
Reference in a new issue