I am using leaflet with openstreetmap to create a fixed grid on top of the world map that consists of 100m x 100m tiles. Basically, I am creating a turn-based game, where a player should be able to click on a certain tile, which then reveals a context menu. The server is going to know that the player has opened the tile for a certain place.
I tried the following:
<!DOCTYPE html>
<html>
<head>
<title>GridLayer Test</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.1/dist/leaflet.css" />
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet#1.0.1/dist/leaflet.js"></script>
<script>
var map = new L.Map('map', { center: [10, 0], zoom: 2 });
var tiles = new L.GridLayer();
tiles.createTile = function (coords) {
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
var ctx = tile.getContext('2d');
var size = this.getTileSize()
tile.width = size.x
tile.height = size.y
// calculate projection coordinates of top left tile pixel
var nwPoint = coords.scaleBy(size)
// calculate geographic coordinates of top left tile pixel
var nw = map.unproject(nwPoint, coords.z)
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, size.x, 50);
ctx.fillStyle = 'black';
ctx.fillText('x: ' + coords.x + ', y: ' + coords.y + ', zoom: ' + coords.z, 20, 20);
ctx.fillText('lat: ' + nw.lat + ', lon: ' + nw.lng, 20, 40);
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size.x - 1, 0);
ctx.lineTo(size.x - 1, size.y - 1);
ctx.lineTo(0, size.y - 1);
ctx.closePath();
ctx.stroke();
return tile;
}
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap',
minNativeZoom: 1,
maxNativeZoom: 1,
}).addTo(map)
tiles.addTo(map)
</script>
</body>
</html>
As you can see the grid changed when I zoom in or out, even though I used minNativeZoom. However, I would like to have the grid fixed and 100m x 100m wide.
I also tried to only return tile when zoomLevel = 18. This does not work.
Any suggestions what I am doing wrong?
I appreciate your replies!
You can draw a grid with the following createTile implementation:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>GridLayer Test</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet#1.0.1/dist/leaflet.css" />
<style>
body {
padding: 0;
margin: 0;
}
html,
body,
#map {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet#1.0.1/dist/leaflet.js"></script>
<script>
const numTilesX = 2 ** 17
const numTilesY = 2 ** 17
class TileNumber {
constructor(x, y) {
this.x = x;
this.y = y;
}
equals(other) {
return this.x === other.x && this.y === other.y;
}
}
let coloredTiles = [
new TileNumber(70435, 45249),
new TileNumber(70434, 45248),
new TileNumber(70441, 45245)
]
function latLngToTileNumber(latLng) {
const lngDegrees = latLng.lng;
const latRadians = latLng.lat * (Math.PI/180);
return new L.Point(
numTilesX * ((lngDegrees + 180) / 360),
numTilesY * (1 - Math.log(Math.tan(latRadians) + 1 / Math.cos(latRadians)) / Math.PI) / 2
);
}
const map = new L.Map('map', {center: [48.5748229, 13.4609744], zoom: 16, maxZoom: 19});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap', maxZoom: 19
}).addTo(map)
const tiles = new L.GridLayer({minZoom: 12});
tiles.createTile = function (coords) {
const tile = L.DomUtil.create('canvas', 'leaflet-tile');
const ctx = tile.getContext('2d');
const size = this.getTileSize();
tile.width = size.x
tile.height = size.y
// calculate projection coordinates of top left tile pixel
const nwPoint = coords.scaleBy(size);
// calculate geographic coordinates of top left tile pixel
const nw = map.unproject(nwPoint, coords.z);
// calculate fraction tile number at top left point
const nwTile = latLngToTileNumber(nw, Math.floor)
// calculate projection coordinates of bottom right tile pixel
const sePoint = new L.Point(nwPoint.x + size.x - 1, nwPoint.y + size.y - 1)
// calculate geographic coordinates of bottom right tile pixel
const se = map.unproject(sePoint, coords.z);
// calculate fractional tile number at bottom right point
const seTile = latLngToTileNumber(se, Math.ceil)
const minTileX = nwTile.x
const maxTileX = seTile.x
const minTileY = nwTile.y
const maxTileY = seTile.y
for (let x = Math.ceil(minTileX) - 1; x <= Math.floor(maxTileX) + 1; x++) {
for (let y = Math.ceil(minTileY) - 1; y <= Math.floor(maxTileY) + 1; y++) {
let tile = new TileNumber(x, y)
const xMinPixel = Math.round(size.x * (x - minTileX) / (maxTileX - minTileX));
const xMaxPixel = Math.round(size.x * (x + 1 - minTileX) / (maxTileX - minTileX));
const yMinPixel = Math.round(size.y * (y - minTileY) / (maxTileY - minTileY));
const yMaxPixel = Math.round(size.y * (y + 1 - minTileY) / (maxTileY - minTileY));
// fill the rectangle with a color
ctx.fillStyle = coloredTiles.some(t => t.equals(tile))
? 'rgba(0, 0, 255, 0.3)'
: 'rgba(255, 255, 255, 0)';
ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);
if (coords.z >= 16) {
// draw the white rectangle and text at the top of the cell
ctx.fillStyle = 'white';
ctx.fillRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, 28);
ctx.fillStyle = 'black';
ctx.font = "15px Arial"
ctx.fillText(tile.x + "," + tile.y, xMinPixel + 10, yMinPixel + 20, xMaxPixel - xMinPixel);
}
if (coords.z >= 13) {
// draw a border
ctx.strokeStyle = 'black';
ctx.strokeRect(xMinPixel, yMinPixel, xMaxPixel - xMinPixel, yMaxPixel - yMinPixel);
}
}
}
return tile;
}
tiles.addTo(map);
map.on('click', e => {
const fractionalTileNumber = latLngToTileNumber(e.latlng);
const tileNumber = new TileNumber(Math.floor(fractionalTileNumber.x), Math.floor(fractionalTileNumber.y));
console.log("Tile " + tileNumber.x + " " + tileNumber.y + " clicked");
if (coloredTiles.some(t => t.equals(tileNumber))) {
coloredTiles = coloredTiles.filter(t => !t.equals(tileNumber));
} else {
coloredTiles.push(tileNumber);
}
tiles.redraw();
});
</script>
</body>
</html>
Some caveats:
Because the earth isn't flat, it's not really possible to cleanly cover it with a grid of rectangles. So I did the closest thing to it by drawing grid boundaries along latitude and longitude lines. As a result, the tiles will get larger (cover more square meters) towards the equator and smaller towards the poles.
Each grid cell has an unique TileNumber (x and y coordinate, starting at 0,0 in the northwestern corner).
To demonstrate clicking on the map, I'm writing the TileNumber of the to the log and toggle the tile's colored/uncolored state. Of course, this could be replaced with any other functionality imaginable, including communication with a server.
As this snippet includes the ability to color the grid cells based on their TileNumber (stored in an array), I've provided this as an answer to your question about coloring grid tiles as well.
Using the sample code from Konvajs.org as a base (https://konvajs.org/docs/sandbox/Multi-touch_Scale_Stage.html), I have added a large SVG to a layer (4096 x 3444) to experiment with zoom / pan of a vector-based map, base64 encoded SVG in this instance. Initial impressions are good however during testing I experience an odd bug where during a pinch the view of the map would snap to a different location on the map not the area that I centred on.
Here is the code (map base64 code removed due to length):
// by default Konva prevent some events when node is dragging
// it improve the performance and work well for 95% of cases
// we need to enable all events on Konva, even when we are dragging a node
// so it triggers touchmove correctly
Konva.hitOnDragEnabled = true;
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Konva.Stage({
container: 'container',
width: width,
height: height,
draggable: true,
});
var layer = new Konva.Layer();
var triangle = new Konva.RegularPolygon({
x: 190,
y: stage.height() / 2,
sides: 3,
radius: 80,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
});
var circle = new Konva.Circle({
x: 380,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
});
let bg = new Konva.Image({
width: 4096,
height: 3444
});
layer.add(bg);
var image = new Image();
image.onload = function() {
bg.image(image);
layer.draw();
};
image.src = 'data:image/svg+xml;base64,...';
function getDistance(p1, p2) {
return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
}
function getCenter(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2,
};
}
var lastCenter = null;
var lastDist = 0;
stage.on('touchmove', function (e) {
e.evt.preventDefault();
var touch1 = e.evt.touches[0];
var touch2 = e.evt.touches[1];
if (touch1 && touch2) {
// if the stage was under Konva's drag&drop
// we need to stop it, and implement our own pan logic with two pointers
if (stage.isDragging()) {
stage.stopDrag();
}
var p1 = {
x: touch1.clientX,
y: touch1.clientY,
};
var p2 = {
x: touch2.clientX,
y: touch2.clientY,
};
if (!lastCenter) {
lastCenter = getCenter(p1, p2);
return;
}
var newCenter = getCenter(p1, p2);
var dist = getDistance(p1, p2);
if (!lastDist) {
lastDist = dist;
}
// local coordinates of center point
var pointTo = {
x: (newCenter.x - stage.x()) / stage.scaleX(),
y: (newCenter.y - stage.y()) / stage.scaleX(),
};
var scale = stage.scaleX() * (dist / lastDist);
stage.scaleX(scale);
stage.scaleY(scale);
// calculate new position of the stage
var dx = newCenter.x - lastCenter.x;
var dy = newCenter.y - lastCenter.y;
var newPos = {
x: newCenter.x - pointTo.x * scale + dx,
y: newCenter.y - pointTo.y * scale + dy,
};
stage.position(newPos);
lastDist = dist;
lastCenter = newCenter;
}
});
stage.on('touchend', function () {
lastDist = 0;
lastCenter = null;
});
layer.add(triangle);
layer.add(circle);
stage.add(layer);
I am unsure if this is due to the large size of the image and / or canvas or an inherent flaw in the example code from Konvas.js. This has been tested, with the same results, on 2 models of iPad Pro, iPhone X & 11, Android Pixel 3, 5 and 6 Pro.
Here is the code on codepen as an example: https://codepen.io/mr-jose/pen/WNXgbdG
Any help would be appreciated, thanks!
I faced the same issue and discovered that it was caused by the dragging functionality of the stage. Everytime if (stage.isDragging()) evaluated to true, the jump happened.
For me what worked was setting draggable to false while pinch zooming and back to true on touch end.
stage.on('touchmove', function (e) {
...
if (touch1 && touch2) {
stage.draggable(false);
....
}
});
stage.on('touchend', function (e) {
...
stage.draggable(true);
});
I know in traditional HTML5 canvas, we can use drawImage method (the longest one with 9 properties) and change frameX and frameY to make sprite sheet animation. But I am new to matter.js. I've checked matter.js document but still don't have any idea about how to animate my sprite. Here is my object:
const ball = Bodies.circle(340, 340, 10, {
density: 0.0005,
frictionAir: 0.06,
restitution: 0,
friction: 0,
render: {
sprite: {
texture: "images/blueMonster.png",
yScale: 0.2,
xScale: 0.2,
isStatic: true,
},
},
inertia: Infinity,
label: "ball",
});
World.add(world, ball);
If I need to provide more info to solve this problem, please let me know. Thank you very much for your time!
There may be a fundamental misconception here. Matter.js is a physics engine that can plug into any rendering front-end. You don't have to use the built-in MJS rendering engine which is primarily there for prototyping. You can use your existing HTML5 code or something like Phaser which has robust support for sprite sheets.
Here's a simple proof-of-concept using vanilla JS to render a sprite animation with MJS as the physics engine. The approach is to call Matter.Engine.update(engine); to run the engine each frame and use coin.position to draw the sprite. More complex animations might use vertices and angle as shown here and here in addition to the sprite sheet, but this is use-case dependent.
(async () => {
const image = await new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = reject;
image.src = "https://art.pixilart.com/c7f297523ce57fc.png";
});
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 500;
canvas.height = 250;
const engine = Matter.Engine.create();
const coin = Matter.Bodies.circle(100, 0, 100, {
density: 0.0005,
frictionAir: 0.06,
restitution: 0,
friction: 0,
});
const ground = Matter.Bodies.rectangle(
0, 350, 1500, 170, {isStatic: true}
);
const mouseConstraint = Matter.MouseConstraint.create(
engine, {element: canvas}
);
Matter.Composite.add(
engine.world, [coin, ground, mouseConstraint]
);
const w = 200;
const h = 170;
let frameNumber = 0;
(function rerender() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const offset = (~~frameNumber * w) % image.width;
const {x, y} = coin.position;
ctx.drawImage(
image, // image
offset, // sx
40, // sy
w, // sWidth
h, // sHeight
x - w / 2, // dx
y - h / 2, // dy
w, // dWidth
h // dHeight
);
frameNumber += 0.1;
Matter.Engine.update(engine);
requestAnimationFrame(rerender);
})();
})();
canvas {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<canvas></canvas>
Another approach is to plop in a gif or video--easily done since you're just making a webpage in the DOM that happens to have physics attached. See how to round an image in matter.js for an example.
I was working on canvas and came across the Idea of changing dimensions of the cube. So, by using HTML5 Canvas I made up this cube which has two squares joined by the lines to make it look like a cube.
What I want is when I select a cube type from select the cube should automatically change itself depending on the length and width of the selected option. The height remains constant. Like if the I select the cube of 5x5 which is by default a cube but when the I select the option of 5x10 the width(front) should not be changed but the length(side) of the cube should expand, and vice versa if I select 10x5 my max option is 25x15. As you can see the canvas I created below is in pixels, first I need to convert these pixels into centimeters(cm) then centimeters to cubic meters.
The whole cube should be aligned in the fixed canvas area specified.
Here is fiddle
var canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 300;
var contxt = canvas.getContext('2d');
//squares
/*
contxt.fillRect(x, y, widht, height);
*/
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(80, 80, 100, 100);
contxt.rect(120, 40, 100, 100);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
/*Lines
contxt.beginPath();
contxt.moveTo(x, y);
contxt.lineTo(300, 100);
*/
contxt.beginPath();
contxt.moveTo(80, 80);
contxt.lineTo(120, 40);
contxt.moveTo(180, 80);
contxt.lineTo(220, 40);
contxt.moveTo(80, 180);
contxt.lineTo(120, 140);
contxt.moveTo(180, 180);
contxt.lineTo(220, 140);
contxt.stroke();
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select>
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing the cube:
To generate a dynamic cube you would have to listen to an onChange event on the <select> element. Every time the selected option changes you would want to redraw your cube.
To redraw the cube you need to create a renderCube function which should take the new dimensions of the cube and as specified an offset for positioning. In this function you have to clear the previously drawn cube and redraw the new one with the given dimensions and offset.
Adding a transition effect:
As you can not apply css transitions to canvas elements you have to implement the transition yourself. You would have to create an animation function which would calculate the dimensions of the cube in the transition phase and rerender it to the screen on each frame.
An implementation of the resizable cube with a transition effect would be:
(if you prefer here is a fiddle too)
(if you do not need the transition effect check the fiddle before it has been implemented)
var canvas = document.querySelector('canvas');
canvas.width = 320;
canvas.height = 150;
var contxt = canvas.getContext('2d');
var currentHeight = 0, currentWidth = 0, currentDepth = 0, animationId = 0;
function renderCube(height, width, depth, offsetX, offsetY) {
currentHeight = height;
currentWidth = width;
currentDepth = depth;
// Clear possible existing cube
contxt.clearRect(0, 0, canvas.width, canvas.height);
contxt.beginPath();
// Calculate depth, width and height based on given input
depth = (depth * 10 * 0.8) / 2;
width = width * 10;
height = height * 10;
// Draw 2 squares to the canvas
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(offsetX, offsetY, width, height);
contxt.rect(offsetX + depth, offsetY - depth, width, height);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
// An array which specifies where to draw the depth lines between the 2 rects
// The offset will be applied while drawing the lines
var depthLineCoordinates = [
// posX, posY, posX2, posY2
[0, 0, depth, -depth],
[width, 0, width + depth, -depth],
[0, height, depth, height - depth],
[width, height, width + depth, height - depth]
];
// Draw the depth lines to the canvas
depthLineCoordinates.forEach(function(element) {
contxt.moveTo(offsetX + element[0], offsetY + element[1]);
contxt.lineTo(offsetX + element[2], offsetY + element[3]);
});
contxt.stroke();
}
// As requested by OP an example of a transition to the cube
// The transitionDuration may be a double which specifies the transition duration in seconds
function renderCubeWithTransistion(height, width, depth, offsetX, offsetY, transitionDuration) {
var fps = 60;
var then = Date.now();
var startTime = then;
var finished = false;
var heightDifference = (height - currentHeight);
var widthDifference = (width - currentWidth);
var depthDifference = (depth - currentDepth);
// Get an "id" for the current animation to prevent multiple animations from running at the same time.
// Only the last recently started animation will be executed.
// If a new one should be run, the last one will get aborted.
var transitionStartMillis = (new Date()).getMilliseconds();
animationId = transitionStartMillis;
function animate() {
// Do not continue rendering the current animation if a new one has been started
if (transitionStartMillis != animationId) return;
// request another frame if animation has not been finished
if (!finished) requestAnimationFrame(animate);
// Control FPS
now = Date.now();
elapsed = now - then;
if (elapsed > (1000 / fps)) {
then = now - (elapsed % (1000 / fps));
// Calculate a linear transition effect
if (parseInt(currentHeight, 0) != parseInt(height, 0)) currentHeight += heightDifference / (transitionDuration * fps);
if (parseInt(currentWidth, 0) != parseInt(width, 0)) currentWidth += widthDifference / (transitionDuration * fps);
if (parseInt(currentDepth, 0) != parseInt(depth, 0)) currentDepth += depthDifference / (transitionDuration * fps);
// Render the cube
renderCube(currentHeight, currentWidth, currentDepth, offsetX, offsetY);
// Check if the current dimensions of the cube are equal to the specified dimensions of the cube
// If they are the same, finish the transition
if (parseInt(currentHeight, 0) === parseInt(height, 0) && parseInt(currentWidth, 0) === parseInt(width, 0) && parseInt(currentDepth, 0) === parseInt(depth, 0)) {
finished = true;
}
}
}
// Start the animation process
animate();
return true;
}
// Draw the cube initially with 5x5
renderCube(5, 5, 5, 80, 70);
// Add the onChange event listener to the select element
var cubeSizeSelector = document.getElementById('cubeSizeSelector');
cubeSizeSelector.onchange = function(e) {
var cubeSize = e.target.value.split('x');
renderCubeWithTransistion(5, parseInt(cubeSize[0], 0), parseInt(cubeSize[1], 0), 80, 70, 0.3);
}
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> </script>
<select id="cubeSizeSelector">
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing an extruded outline. Axonometric
Ideally you would create a generic axonometric renderer that given a floor plan renders the object to the canvas as needed.
You can then link the plan to a selection box and update the view when the selection has changed.
Best as a code example
The example below uses the object renderIsoPlan to render the shape.
Shapes are set via a plan. eg a box has a floor plan [[-1,-1],[1,-1],[1,1],[-1,1]] representing the 4 bottom corners.
The renderIsoPlan has the following properties
canvas The canvas that the shape is rendered to. Will not draw until this is set. renderIsoPlan will create a 2D context which will be the same if you have one already
height How far up the outline is projected.
style Canvas context style object eg {stokeStyle : "red", lineWidth : 2} draws 2 pixel with red lines.
plan Set of points for the floor. Points are moved to center automatically. eg [[0,-1],[1,1],[-1,1]] draws a triangle
scale Scale say no more
rotate Amount to rotate. If not 0 then projection is dimetric else it is trimetric.
centerY in unit size of canvas. ie 0.5 is center
centerX same as centerY
Call renderIsoPlan.refresh to draw
Note that you can not rotate the projection in the question as it visually appears to warp (change shape) thus if rotate is not 0 then a different projection is used.
Note the object is automatically centered around 0,0 use centerX, centerY to center in the view
setTimeout(start,0); // wait till Javascript parsed and executed
requestAnimationFrame(animate); // Animate checked at start so start anim
// named list of shapes
const boxes = {
box1By1 : {
plan : [[-1,-1],[1,-1],[1,1],[-1,1]],
scale : 35,
centerY : 0.75,
},
box1By2 : {
plan : [[-1,-2],[1,-2],[1,2],[-1,2]],
scale : 30,
centerY : 0.7,
},
box2By2 : {
plan : [[-2,-2],[2,-2],[2,2],[-2,2]],
scale : 25,
centerY : 0.7,
},
box2By1 : {
plan : [[-2,-1],[2,-1],[2,1],[-2,1]],
scale : 30,
centerY : 0.7,
},
box1By3 : {
plan : [[-1,-3],[1,-3],[1,3],[-1,3]],
scale : 22,
centerY : 0.67,
},
box1By4 :{
plan : [[-1,-4],[1,-4],[1,4],[-1,4]],
scale : 20,
centerY : 0.63,
},
lShape : {
plan : [[-2,-4],[0,-4],[0,2],[2,2],[2,4],[-2,4]],
scale : 20,
centerY : 0.65,
},
current : null,
}
// Sets the renderIsoPlan object to the current selection
function setShape(){
boxes.current = boxes[boxShape.value];
Object.assign(renderIsoPlan, boxes.current);
if (!animateCheckBox.checked) { renderIsoPlan.refresh() }
}
// When ready this is called
function start(){
renderIsoPlan.canvas = canvas;
renderIsoPlan.height = 2;
setShape();
renderIsoPlan.refresh();
}
// Add event listeners for checkbox and box selection
boxShape.addEventListener("change", setShape );
animateCheckBox.addEventListener("change",()=>{
if (animateCheckBox.checked) {
requestAnimationFrame(animate);
} else {
renderIsoPlan.rotate = 0;
setShape();
}
});
// Renders animated object
function animate(time){
if (animateCheckBox.checked) {
renderIsoPlan.rotate = time / 1000;
renderIsoPlan.refresh();
requestAnimationFrame(animate);
}
}
// Encasulate Axonometric render.
const renderIsoPlan = (() => {
var ctx,canvas,plan,cx,cy,w,h,scale,height, rotate;
height = 50;
scale = 10;
rotate = 0;
const style = {
strokeStyle : "#000",
lineWidth : 1,
lineJoin : "round",
lineCap : "round",
};
const depthScale = (2/3);
// Transforms then projects the point to 2D
function transProjPoint(p) {
const project = rotate !== 0 ? 0 : depthScale;
const xdx = Math.cos(rotate);
const xdy = Math.sin(rotate);
const y = p[0] * xdy + p[1] * xdx;
const x = p[0] * xdx - p[1] * xdy - y * project;
return [x,y * depthScale];
}
// draws the plan
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,w,h);
ctx.setTransform(scale, 0, 0, scale, cx, cy);
var i = plan.length;
ctx.beginPath();
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
ctx.translate(0,-height);
ctx.moveTo(...transProjPoint(plan[--i]))
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
while(i--){
const [x,y] = transProjPoint(plan[i]);
ctx.moveTo(x,y);
ctx.lineTo(x,y + height);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
// centers the plan view on coordinate 0,0
function centerPlan(plan){
var x = 0, y = 0;
for(const point of plan){
x += point[0];
y += point[1];
}
x /= plan.length;
y /= plan.length;
for(const point of plan){
point[0] -= x;
point[1] -= y;
}
return plan;
}
// Sets the style of the rendering
function setStyle(){
for(const key of Object.keys(style)){
if(ctx[key] !== undefined){
ctx[key] = style[key];
}
}
}
// define the interface
const API = {
// setters allow the use of Object.apply
set canvas(c) {
canvas = c;
ctx = canvas.getContext("2d");
w = canvas.width; // set width and height
h = canvas.height;
cx = w / 2 | 0; // get center
cy = h / 2 | 0; // move center down because plan is extruded up
},
set height(hh) { height = hh },
set style(s) { Object.assign(style,s) },
set plan(points) { plan = centerPlan([...points]) },
set scale(s) { scale = s },
set rotate(r) { rotate = r },
set centerY(c) { cy = c * h },
set centerX(c) { cx = c * w },
// getters not used in the demo
get height() { return height },
get style() { return style },
get plan() { return plan },
get scale() { return scale },
get rotate() { return r },
get centerY() { return cy / h },
get centerX() { return cx / w },
// Call this to refresh the view
refresh(){
if(ctx && plan){
ctx.save();
if(style){ setStyle() }
draw();
ctx.restore();
}
}
}
// return the interface
return API;
})();
canvas { border : 2px solid black; }
<select id="boxShape">
<option value = "box1By1">1 by 1</option>
<option value = "box1By2">1 by 2</option>
<option value = "box2By2">2 by 2</option>
<option value = "box2By1">2 by 1</option>
<option value = "box1By3">1 by 3</option>
<option value = "box1By4">1 by 4</option>
<option value = "lShape">L shape</option>
</select>
<input type="checkBox" id="animateCheckBox" checked=true>Animate</input><br>
<canvas id="canvas"></canvas>
I have t his effect on this website
http://www.immersive-garden.com/
there's this point of light sparking, on hover you get the background, I want something similar without using flash
this is the script I'm using right now
/*
Particle Emitter JavaScript Library
Version 0.3
by Erik Friend
Creates a circular particle emitter of specified radius centered and offset at specified screen location. Particles appear outside of emitter and travel outward at specified velocity while fading until disappearing in specified decay time. Particle size is specified in pixels. Particles reduce in size toward 1px as they decay. A custom image(s) may be used to represent particles. Multiple images will be cycled randomly to create a mix of particle types.
example:
var emitter = new particle_emitter({
image: ['resources/particle.white.gif', 'resources/particle.black.gif'],
center: ['50%', '50%'], offset: [0, 0], radius: 0,
size: 6, velocity: 40, decay: 1000, rate: 10
}).start();
*/
particle_emitter = function (opts) {
// DEFAULT VALUES
var defaults = {
center: ['50%', '50%'], // center of emitter (x / y coordinates)
offset: [0, 0], // offset emitter relative to center
radius: 0, // radius of emitter circle
image: 'particle.gif', // image or array of images to use as particles
size: 1, // particle diameter in pixels
velocity: 10, // particle speed in pixels per second
decay: 500, // evaporation rate in milliseconds
rate: 10 // emission rate in particles per second
};
// PASSED PARAMETER VALUES
var _options = $.extend({}, defaults, opts);
// CONSTRUCTOR
var _timer, _margin, _distance, _interval, _is_chrome = false;
(function () {
// Detect Google Chrome to avoid alpha transparency clipping bug when adjusting opacity
if (navigator.userAgent.indexOf('Chrome') >= 0) _is_chrome = true;
// Convert particle size into emitter surface margin (particles appear outside of emitter)
_margin = _options.size / 2;
// Convert emission velocity into distance traveled
_distance = _options.velocity * (_options.decay / 1000);
// Convert emission rate into callback interval
_interval = 1000 / _options.rate;
})();
// PRIVATE METHODS
var _sparkle = function () {
// Pick a random angle and convert to radians
var rads = (Math.random() * 360) * (Math.PI / 180);
// Starting coordinates
var sx = parseInt((Math.cos(rads) * (_options.radius + _margin)) + _options.offset[0] - _margin);
var sy = parseInt((Math.sin(rads) * (_options.radius + _margin)) + _options.offset[1] - _margin);
// Ending Coordinates
var ex = parseInt((Math.cos(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[0] - 0.5);
var ey = parseInt((Math.sin(rads) * (_options.radius + _distance + _margin + 0.5)) + _options.offset[1] - 0.5);
// Pick from available particle images
var image;
if (typeof(_options.image) == 'object') image = _options.image[Math.floor(Math.random() * _options.image.length)];
else image = _options.image;
// Attach sparkle to page, then animate movement and evaporation
var s = $('<img>')
.attr('src', image)
.css({
zIndex: 10,
position: 'absolute',
width: _options.size + 'px',
height: _options.size + 'px',
left: _options.center[0],
top: _options.center[1],
marginLeft: sx + 'px',
marginTop: sy + 'px'
})
.appendTo('body')
.animate({
width: '1px',
height: '1px',
marginLeft: ex + 'px',
marginTop: ey + 'px',
opacity: _is_chrome ? 1 : 0
}, _options.decay, 'linear', function () { $(this).remove(); });
// Spawn another sparkle
_timer = setTimeout(function () { _sparkle(); }, _interval);
};
// PUBLIC INTERFACE
// This is what gets returned by "new particle_emitter();"
// Everything above this point behaves as private thanks to closure
return {
start:function () {
clearTimeout(_timer);
_timer = setTimeout(function () { _sparkle(); }, 0);
return(this);
},
stop:function () {
clearTimeout(_timer);
return(this);
},
centerTo:function (x, y) {
_options.center[0] = x;
_options.center[1] = y;
},
offsetTo:function (x, y) {
if ((typeof(x) == 'number') && (typeof(y) == 'number')) {
_options.center[0] = x;
_options.center[1] = y;
}
}
}
};
you probably need something like this: http://www.realcombiz.com/2012/09/customize-blackquote-with-light-bulb.html