simple z-buffer implementation example in JS? [beginner] - javascript

I'm looking for a very basic implementation of the Z-buffer, ideally in JS. I am trying to take a look at a very simple code, for example two polygons overlapping, one hiding the other.
I can't find such basic example, while I can find a couple of "well-above my current level and understanding" samples.
Is there a resource you could recommend to get started?
Thank you for your help and recommendations!

A Z-Buffer(also known as depth buffer) is nothing more than a 2D pixel array(think image). Instead of RGB it only stores a single value in each pixel, the distance from the current viewpoint:
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
It augments the color buffer that contains the actual image you present to the user:
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
For every pixel of a shape you draw you check the Z-Buffer to see if there's anything closer to the camera that occludes the current pixel, if so you don't draw it. This way you can draw things in any order and they're still properly occluded on a per pixel level.
Z-Buffering may not only be used in 3D but also in 2D to achieve draw order-independence. Lets say we want to draw a few boxes, this will be our box class:
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
With this class we can now create a few boxes and draw them:
const boxes = [
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 50, height: 20 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 50 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 60, y: 55, z: 8 },
size: { width: 50, height: 10 },
color: { r: 0, g: 0, b: 255 }
})
];
With our shapes specified we can now draw them:
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
// this is commonly referred to as "depth-test"
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint than our current primitive, don't draw this pixel:
continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
Note that this code is exemplary so it's overly verbose and inefficient for the sake of laying out the concept.
const ctx = document.getElementById("output").getContext('2d');
const screenWidth = 200;
const screenHeight = 200;
// one value for each pixel in our screen
const depthBuffer = new Array(screenWidth * screenHeight);
// create buffer for color output
const numChannels = 3; // R G B
const colorBuffer = new Array(screenWidth * screenHeight * numChannels);
/**
* Represents a 2D box
* #class
*/
class Box {
/** #member {Object} position of the box storing x,y,z coordinates */
position;
/** #member {Object} size of the box storing width and height */
size;
/** #member {Object} color of the box given in RGB */
color;
constructor (props) {
this.position = props.position;
this.size = props.size;
this.color = props.color;
}
/**
* Check if given point is in box
* #param {Number} px coordinate of the point
* #param {Number} py coordinate of the point
* #return {Boolean} point in box
*/
pointInBox (px,py) {
return this.position.x < px && this.position.x + this.size.width > px
&& this.position.y < py && this.position.y + this.size.height > py;
}
}
const boxes = [
// red box
new Box({
position: { x: 50, y: 50, z: 10 },
size: { width: 150, height: 50 },
color: { r: 255, g: 0, b:0 }
}),
// green box
new Box({
position: { x: 80, y: 30, z: 5 },
size: { width: 10, height: 150 },
color: { r: 0, g: 255, b:0 }
}),
// blue
new Box({
position: { x: 70, y: 70, z: 8 },
size: { width: 50, height: 40 },
color: { r: 0, g: 0, b: 255 }
})
];
const varyZ = document.getElementById('varyz');
varyZ.onchange = draw;
function draw () {
// clear depth buffer of previous frame
depthBuffer.fill(10);
for(const box of boxes) {
for(let x = 0; x < screenWidth; x++) {
for(let y = 0; y < screenHeight; y++) {
// check if our pixel is within the box
if (box.pointInBox(x,y)) {
// check if this pixel of our box is covered by something else
// compare depth value in depthbuffer against box position
if (depthBuffer[x + y * screenWidth] < box.position.z) {
// something is already closer to the viewpoint that our current primitive, don't draw this pixel:
if (!varyZ.checked) continue;
if (depthBuffer[x + y * screenWidth] < box.position.z + Math.sin((x+y))*Math.cos(x)*5) continue;
}
// we passed the depth test, put our current depth value in the z-buffer
depthBuffer[x + y * screenWidth] = box.position.z;
// put the color in the color buffer, channel by channel
colorBuffer[(x + y * screenWidth)*numChannels + 0] = box.color.r;
colorBuffer[(x + y * screenWidth)*numChannels + 1] = box.color.g;
colorBuffer[(x + y * screenWidth)*numChannels + 2] = box.color.b;
}
}
}
}
// convert to rgba for presentation
const oBuffer = new Uint8ClampedArray(screenWidth*screenHeight*4);
for (let i=0,o=0; i < colorBuffer.length; i+=3,o+=4) {
oBuffer[o]=colorBuffer[i];
oBuffer[o+1]=colorBuffer[i+1];
oBuffer[o+2]=colorBuffer[i+2];
oBuffer[o+3]=255;
}
ctx.putImageData(new ImageData(oBuffer, screenWidth, screenHeight),0,0);
}
document.getElementById('redz').oninput = e=>{boxes[0].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('greenz').oninput = e=>{boxes[1].position.z=parseInt(e.target.value,10);draw()};
document.getElementById('bluez').oninput = e=>{boxes[2].position.z=parseInt(e.target.value,10);draw()};
draw();
canvas {
border:1px solid black;
float:left;
margin-right: 2rem;
}
label {display:block;}
label span {
display:inline-block;
width: 100px;
}
<canvas width="200" height="200" id="output"></canvas>
<label><span>Red Z</span>
<input type="range" min="0" max="10" value="10" id="redz"/>
</label>
<label><span>Green Z</span>
<input type="range" min="0" max="10" value="5" id="greenz"/>
</label>
<label><span>Blue Z</span>
<input type="range" min="0" max="10" value="8" id="bluez"/>
</label>
<label><span>Vary Z Per Pixel</span>
<input type="checkbox" id="varyz"/>
</label>

Related

Merging two different sized intersecting circles and finding the coordinates of a radius-weighted center

I'm working on a fun little simulation environment for circles. I cannot find an accurate way to combine two circles and find their center coordinate.
I set up an html canvas, then generate random coords on the plane along with a random sized radius. After every generation, I check for an intersection between every circle and every other circle. When circles intersect I want them to merge - making a circle with the combined surface area. Finding the coordinates of the new center is my issue.
I don't want to simply find the midpoint of the centers because that doesn't factor in the size of the circles. A humongous circle could be swayed by a tiny one, which doesn't make for a realistic simulation.
I've thought up what I think is a bad solution: multiplying the change in distance created by the midpoint formula by the ratio of the two circles radii, getting the angle of the resulting triangle, using trig to get the x and y difference, then adding that to the center of the larger circle and calling it a day.
Really have no clue if that is the right way to do it, so I wanted to ask people smarter than me.
Oh also here's a link to the repo on github:
Circle Simulator
This is my first stackOverflow question so bear with me if I did something completely stupid. Thanks everyone!
var dataForm = document.getElementById('dataForm');
var type = document.getElementById('type');
var dataMinRad = document.getElementById('dataMinRad');
var dataMaxRad = document.getElementById('dataMaxRad');
var phaseInterval = document.getElementById('phaseInterval');
//form on submit
const onDataSubmit = (e) => {
if (e) e.preventDefault();
//updates min and max radius
minRadius = parseInt(dataMinRad.value);
maxRadius = parseInt(dataMaxRad.value);
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//clears circles
circles = [];
//clears any previous interval
clearInterval(phase);
let generator = eval(type.value), data;
//every one second this code is repeated
phase = setInterval(() => {
//gets the circle data from whatever generator is selected
data = generator();
//adds the new circle and draws it on the canvas if the data is good
if (data) {
circles.push(new Circle(data.x, data.y, data.rad));
circles[circles.length - 1].draw();
}
}, parseInt(phaseInterval.value));
}
dataForm.addEventListener('submit', onDataSubmit);
</script>
<script>
//initializes global elements
var stage = document.getElementById('stage');
var canvas = document.getElementById('myCanvas');
var c = canvas.getContext('2d');
//sets width and height of canvas to that of the stage
canvas.setAttribute('width', stage.clientWidth);
canvas.setAttribute('height', stage.clientHeight);
class Circle {
constructor (x, y, rad) {
this.x = x;
this.y = y;
this.rad = rad;
}
draw() {
c.fillStyle = 'black';
c.beginPath();
c.arc(this.x, this.y, this.rad, 0, 2 * Math.PI, true);
c.stroke();
}
}
//variables
var circles = [];
var maxRadius = 100;
var minRadius = 1;
var phase;
const random = () => {
//random coords and radius
let x, y, rad;
do {
[x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius];
} while ((() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) //end while
return { x: x, y: y, rad: rad};
}
const order = () => {
//gets some random coords and sets the radius to max
let [x, y, rad] = [Math.round(Math.random() * canvas.width), Math.round(Math.random() * canvas.height), maxRadius];
//decreases the radius while the resulting circle still intercects any other circle
while (rad >= minRadius && (() => {
for (let i in circles) {
if (Math.sqrt(Math.pow(x - circles[i].x, 2) + Math.pow(y - circles[i].y, 2)) < rad + circles[i].rad) {
return true;
}
}
return false;
})()) {
rad--;
}
//only sends the radii that are greater than the minimum radius
if (rad >= minRadius) return { x: x, y: y, rad: rad};
}
//the position changes must be weighted somehow
const agar = () => {
//some looping control variables
let i = 0, j = 1, noChange = true;
//loops through the circles array in every circle until the noChange variable is false
while (i < circles.length && noChange) {
while (j < circles.length && noChange) {
//checks if each circle is inside each other circle
if (Math.sqrt(Math.pow(circles[i].x - circles[j].x, 2) + Math.pow(circles[i].y - circles[j].y, 2)) < circles[i].rad + circles[j].rad) {
//copies the two circles
let tempCircles = [circles[i], circles[j]];
//splices the item closest to the end of the array first so that the position of the other doesn't shift after the splice
if (i > j) {
circles.splice(i, 1);
circles.splice(j, 1);
} else {
circles.splice(j, 1);
circles.splice(i, 1);
}
//radius of the two circles' surface area combined
let rad = Math.sqrt(tempCircles[0].rad * tempCircles[0].rad + tempCircles[1].rad * tempCircles[1].rad);
/*
// method 1: the midpoint of the centers //
let x = (tempCircles[0].x + tempCircles[1].x) / 2;
let y = (tempCircles[0].y + tempCircles[1].y) / 2;
*/
// method 2: the radius ratio weighted //
let bigCircle, smallCircle;
if (tempCircles[0].rad > tempCircles[1].rad) {
bigCircle = tempCircles[0];
smallCircle = tempCircles[1];
} else {
bigCircle = tempCircles[1];
smallCircle = tempCircles[0];
}
//get the distance between the two circles
let dist = Math.sqrt(Math.pow(bigCircle.x - smallCircle.x, 2) + Math.pow(bigCircle.y - smallCircle.y, 2));
//gets the ratio of the two circles radius size
let radRatio = smallCircle.rad / bigCircle.rad;
//the adjusted hypot for the ratio
dist = dist * radRatio;
//the angle
let theta = Math.atan2(smallCircle.y - bigCircle.y, smallCircle.x - bigCircle.x); // all hail atan2!
//the new center coords
let x = bigCircle.x + dist * Math.cos(theta);
let y = bigCircle.y + dist * Math.sin(theta);
circles.push(new Circle(x, y, rad));
//change happened so the variable should be false
noChange = false;
/*
-find the middle of the point
-weigh it in the direction of teh biggest circle
radius as the magnitude and [angle of the triangle created when the centers are connected] as the direction for both radii.
find the point on each circle closest to the center of the other circle
find those two points midpoint
find the distance from that point to each of the centers
those two distances are the magnitude of two new vectors with the same angels as before
add those two vectors
is there really not a freaking easier way?
*/
/*
try this:
-get the distance between the centers.
-multiply that by the ratio
-get the angle
-use that angle and that hypot to find the x and y
-add the x and y to the bigger circles centerr
*/
}
j++;
}
i++;
j = i + 1;
}
//if there was no change
if (noChange) {
//random coords and radius size
let x = Math.round(Math.random() * canvas.width),
y = Math.round(Math.random() * canvas.height),
rad = Math.ceil(Math.random() * (maxRadius - minRadius)) + minRadius;
//adds the random circle to the array
circles.push(new Circle(x, y, rad));
}
//clears canvas
c.clearRect(0, 0, canvas.width, canvas.height);
//redraws ALL circles
for (let i in circles) {
circles[i].draw();
}
}
onDataSubmit();
* {
margin: 0;
box-sizing: border-box;
}
#wrapper {
width: 100%;
max-width: 1280px;
margin: auto;
margin-right: 0;
display: flex;
flex-flow: row nowrap;
}
#dataContainer {
height: 100%;
width: 20%;
padding: 5px;
}
#dataContainer>* {
padding: 15px;
}
#dataForm {
max-width: 200px;
display: grid;
}
#dataForm>* {
margin-top: 5px;
width: 100%;
}
.center {
margin: auto;
}
#stage {
margin: 5px;
width: 80%;
height: 97vh;
}
<div id='wrapper'>
<!-- form containter -->
<div id='dataContainer'>
<h3>Data</h3>
<form id='dataForm' method='post'>
<label for='type'>Type:</label>
<select id='type' name='type'>
<option value='random' selected>Random</option>
<option value='order'>Order</option>
<option value='agar'>Agario</option>
</select>
<label for='min'>Min-Radius:</label>
<input id='dataMinRad' name='min' type='number' value='1' min='0'>
<label for='max'>Max-Radius:</label>
<input id='dataMaxRad' name='max' type='number' value='100'>
<label for='interval'>Phase Interval:</label>
<input id='phaseInterval' name='interval' type='number' value='1' min='1'>
<button type='submit' id='dataSubmit' class='center'>Load</submit>
</form>
</div>
<!-- canvas container-->
<div id='stage'>
<canvas id='myCanvas'></canvas>
</div>
</div>
So the question is given two overlapping circles find a new circle that represents the merged circles and has an area equal to the sum of the original circles.
For the center of this new circle, one choice is to find the center of mass of the two original circles. If you have two masses of masses m1, m2 and positions (x1,y1), (x2,y2) then the center of mass of the whole system would be
m1/(m1+m2) (x1,y1) + m2/(m1+m2) (x2,y2)
In this case, the center of mass will be
r1^2/(r1^2+r2^2) (x1,y1) + r2^2/(r1^2+r2^2) (x2,y2)
For circles with radii r1, r2 then the masses will be proportional to pi r1^2 + pi r2^2. Hence, the radius of the circle will be sqrt(r1^2+r2^2).
If you can get the center point coordinates and radius, you can draw this new circle
like this:
And about extract a root √ you can use this:
var xxxx = Math.pow(your target here,2);
Update my answer:

Maxpointcount in EllipseSeries - LightningChart JS

I can save memory by setting max point count in scatter chart by following
const pointz = chart.addPointSeries({ pointShape: PointShape.Circle })
.setName('Kuopio')
.setPointFillStyle(fillStyles[0])
.setPointSize(pointSize)
.setMaxPointCount(10000);
But how do I set it same for EllipseSeries?
I dont see any such method like setMaxPointCount for EllipseSeries - https://www.arction.com/lightningchart-js-api-documentation/v1.3.0/classes/ellipseseries.html#add
The EllipseSeries doesn't support the setMaxPointCount functionality. The series type is not meant to be used with a lot of data and as such it doesn't have some of the optimizations that exists for PointSeries, LineSeries and other more basic series types.
You can manually remove points from the EllipseSeries by calling EllipseFigure.dispose() on each ellipse you want to remove from the EllipseSeries. Calling dispose will free up all resources used for rendering the ellipse and remove all references to the ellipse internally. If you remove all references to the ellipse in out own code after calling dispose, all of the memory used by the ellipse will be released.
let ellipse = ellipseSeries.add({x:0,y:0,radiusX: 10,radiusY:10}) // ellipse is rendered here
ellipse.dispose() // ellipse is no longer rendered but some memory is still used.
ellipse = undefined // last reference to the ellipse was removed, all memory is freed
// Extract required parts from LightningChartJS.
const {
lightningChart,
SolidFill,
SolidLine,
ColorRGBA,
emptyFill,
emptyTick,
FontSettings,
AutoCursorModes,
Animator,
AnimationEasings,
UIDraggingModes,
UIOrigins,
ColorPalettes
} = lcjs
// Custom callback template.
const forEachIn = (object, clbk) => { const obj = {}; for (const a in object) obj[a] = clbk(object[a]); return obj }
// Define colors to configure chart and bubbles.
const colors = {
background: ColorRGBA(255, 255, 255),
graphBackground: ColorRGBA(220, 255, 255),
title: ColorRGBA(0, 100, 0),
subTitle: ColorRGBA(0, 100, 0),
bubbleBorder: ColorRGBA(0, 0, 0),
bubbleFillPalette: ColorPalettes.fullSpectrum(100)
}
// Define font settings.
const fonts = {
title: new FontSettings({
size: 40,
weight: 400
})
}
// Create and subtitle with the same font settings, except font-size.
fonts.subTitle = fonts.title.setSize(20)
// Create solid fill styles for defined colors.
const solidFillStyles = forEachIn(colors, (color) => new SolidFill({ color }))
// Create chart with customized settings
const chart = lightningChart().ChartXY({})
.setBackgroundFillStyle(solidFillStyles.background)
.setChartBackgroundFillStyle(solidFillStyles.graphBackground)
.setTitle('Custom Styled Chart')
.setTitleFont(fonts.title)
.setTitleFillStyle(solidFillStyles.title)
.setTitleMarginTop(6)
.setTitleMarginBottom(0)
.setPadding({ left: 5, right: 5, top: 30, bottom: 30 })
.setAutoCursorMode(AutoCursorModes.disabled)
.setMouseInteractionRectangleZoom(undefined)
.setMouseInteractionRectangleFit(undefined)
.setMouseInteractions(false)
// Get axes.
const axes = {
bottom: chart.getDefaultAxisX(),
left: chart.getDefaultAxisY(),
top: chart.addAxisX(true),
right: chart.addAxisY(true).setChartInteractions(false)
}
chart.addUIElement(undefined, { x: chart.uiScale.x, y: axes.right.scale })
.setPosition({ x: 50, y: 10 })
.setOrigin(UIOrigins.CenterBottom)
.setMargin({ bottom: 10 })
.setText('- With Bubbles -')
.setFont(fonts.subTitle)
.setTextFillStyle(solidFillStyles.subTitle)
.setDraggingMode(UIDraggingModes.notDraggable)
// Axis mutator.
const overrideAxis = (axis) => axis
.setTickStyle(emptyTick)
.setTitleMargin(0)
.setNibStyle(line => line.setFillStyle(emptyFill))
.setMouseInteractions(undefined)
// Override default configurations of axes.
for (const axisPos in axes)
overrideAxis(axes[axisPos]);
[axes.bottom, axes.left].forEach(axis => axis.setInterval(-100, 100).setScrollStrategy(undefined))
const bubblePx = {
x: axes.bottom.scale.getPixelSize(),
y: axes.left.scale.getPixelSize()
}
// Create instance of ellipse series to draw bubbles.
const ellipseSeries = chart.addEllipseSeries()
let bubbleCount = 0
// Handler of dragging bubbles.
const bubbleDragHandler = (figure, event, button, startLocation, delta) => {
const prevDimensions = figure.getDimensions()
figure.setDimensions(Object.assign(prevDimensions, {
x: prevDimensions.x + delta.x * figure.scale.x.getPixelSize(),
y: prevDimensions.y + delta.y * figure.scale.y.getPixelSize()
}))
}
// Create resizeBubble array and sizeArray to store the values separately
const resizeBubble = []
const sizeArray = []
// Create a single bubble to visualize in specific coordinates and specified size.
const addBubble = (pos, size) => {
const radius = size * 10
const borderThickness = 1 + size * 1.0
const color = colors.bubbleFillPalette(Math.round(Math.random() * 99))
const fillStyle = new SolidFill({ color })
const strokeStyle = new SolidLine({ fillStyle: solidFillStyles.bubbleBorder, thickness: borderThickness })
const figure = ellipseSeries.add({
x: pos.x,
y: pos.y,
radiusX: radius * bubblePx.x,
radiusY: radius * bubblePx.y
})
.setFillStyle(fillStyle)
.setStrokeStyle(strokeStyle)
// Make draggable by mouse.
figure.onMouseDrag(bubbleDragHandler)
bubbleCount++
return figure
}
// Create an event to handle the case when user resizes the window, the bubble will be automatically scaled
chart.onResize(() => {
for (let i = 0; i <= bubbleMaxCount - 1; i++) {
const newBubble = resizeBubble[i].getDimensions()
resizeBubble[i].setDimensions({
x: newBubble.x,
y: newBubble.y,
radiusX: axes.bottom.scale.getPixelSize() * sizeArray[i] * 10,
radiusY: axes.left.scale.getPixelSize() * sizeArray[i] * 10
})
}
})
// Create a single bubble to visualize in random coordinates and with random size.
const addRandomBubble = () => {
const pos = {
x: Math.random() * 200 - 100,
y: Math.random() * 200 - 100
}
const size = 1 + Math.random() * 7.0
sizeArray.push(size)
resizeBubble.push(addBubble(pos, size))
}
// Amount of bubbles to render.
const bubbleMaxCount = 100
// Animate bubbles creation.
Animator(() => undefined)(2.5 * 1000, AnimationEasings.ease)([[0, bubbleMaxCount]], ([nextBubbleCount]) => {
while (bubbleCount < nextBubbleCount)
addRandomBubble()
})
// dispose all ellipses that have been added before the timeout expires.
setTimeout(()=>{
for(let i =0; i < resizeBubble.length; i++){
resizeBubble[i].dispose()
}
}, 2000)
<script src="https://unpkg.com/#arction/lcjs#1.3.1/dist/lcjs.iife.js"></script>

Change dimension of canvas depending on selected option

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>

changes to javascript effect the page layout

I have been asked by a friend to add a high score table to a game where you maneuver a snake that gets larger as it eats food. My friend wanted me to add a high score to the game.
here is the original code;
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Snake Game</title>
<!-- Basic styling, centering of the canvas. -->
<style>
canvas {
display: block;
position: absolute;
border: 1px solid #000;
margin: auto;
top: 0;
bottom: 0;
right: 0;
left: 0;
}
</style>
</head>
<body>
<script>
var
/**
* Constats
*/
COLS = 26,
ROWS = 26,
EMPTY = 0,
SNAKE = 1,
FRUIT = 2,
LEFT = 0,
UP = 1,
RIGHT = 2,
DOWN = 3,
KEY_LEFT = 37,
KEY_UP = 38,
KEY_RIGHT = 39,
KEY_DOWN = 40,
/**
* Game objects
*/
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score; /* number, keep track of the player score */
high_score = [];
/**
* Grid datastructor, usefull in games where the game world is
* confined in absolute sized chunks of data or information.
*
* #type {Object}
*/
grid = {
width: null, /* number, the number of columns */
height: null, /* number, the number of rows */
_grid: null, /* Array<any>, data representation */
/**
* Initiate and fill a c x r grid with the value of d
* #param {any} d default value to fill with
* #param {number} c number of columns
* #param {number} r number of rows
*/
init: function(d, c, r) {
this.width = c;
this.height = r;
this._grid = [];
for (var x=0; x < c; x++) {
this._grid.push([]);
for (var y=0; y < r; y++) {
this._grid[x].push(d);
}
}
},
/**
* Set the value of the grid cell at (x, y)
*
* #param {any} val what to set
* #param {number} x the x-coordinate
* #param {number} y the y-coordinate
*/
set: function(val, x, y) {
this._grid[x][y] = val;
},
/**
* Get the value of the cell at (x, y)
*
* #param {number} x the x-coordinate
* #param {number} y the y-coordinate
* #return {any} the value at the cell
*/
get: function(x, y) {
return this._grid[x][y];
}
}
/**
* The snake, works as a queue (FIFO, first in first out) of data
* with all the current positions in the grid with the snake id
*
* #type {Object}
*/
snake = {
direction: null, /* number, the direction */
last: null, /* Object, pointer to the last element in
the queue */
_queue: null, /* Array<number>, data representation*/
/**
* Clears the queue and sets the start position and direction
*
* #param {number} d start direction
* #param {number} x start x-coordinate
* #param {number} y start y-coordinate
*/
init: function(d, x, y) {
this.direction = d;
this._queue = [];
this.insert(x, y);
},
/**
* Adds an element to the queue
*
* #param {number} x x-coordinate
* #param {number} y y-coordinate
*/
insert: function(x, y) {
// unshift prepends an element to an array
this._queue.unshift({x:x, y:y});
this.last = this._queue[0];
},
/**
* Removes and returns the first element in the queue.
*
* #return {Object} the first element
*/
remove: function() {
// pop returns the last element of an array
return this._queue.pop();
}
};
/**
* Set a food id at a random free cell in the grid
*/
function setFood() {
var empty = [];
// iterate through the grid and find all empty cells
for (var x=0; x < grid.width; x++) {
for (var y=0; y < grid.height; y++) {
if (grid.get(x, y) === EMPTY) {
empty.push({x:x, y:y});
}
}
}
// chooses a random cell
var randpos = empty[Math.round(Math.random()*(empty.length - 1))];
grid.set(FRUIT, randpos.x, randpos.y);
}
/**
* Starts the game
*/
function main() {
// create and initiate the canvas element
canvas = document.createElement("canvas");
canvas.width = COLS*20;
canvas.height = ROWS*20;
ctx = canvas.getContext("2d");
// add the canvas element to the body of the document
document.body.appendChild(canvas);
// sets an base font for bigger score display
ctx.font = "12px Helvetica";
frames = 0;
keystate = {};
// keeps track of the keybourd input
document.addEventListener("keydown", function(evt) {
keystate[evt.keyCode] = true;
});
document.addEventListener("keyup", function(evt) {
delete keystate[evt.keyCode];
});
// intatiate game objects and starts the game loop
init();
loop();
}
/**
* Resets and inits game objects
*/
function init() {
score = 0;
grid.init(EMPTY, COLS, ROWS);
var sp = {x:Math.floor(COLS/2), y:ROWS-1};
snake.init(UP, sp.x, sp.y);
grid.set(SNAKE, sp.x, sp.y);
setFood();
}
/**
* The game loop function, used for game updates and rendering
*/
function loop() {
update();
draw();
// When ready to redraw the canvas call the loop function
// first. Runs about 60 frames a second
window.requestAnimationFrame(loop, canvas);
}
/**
* Updates the game logic
*/
function update() {
frames++;
// changing direction of the snake depending on which keys
// that are pressed
if (keystate[KEY_LEFT] && snake.direction !== RIGHT) {
snake.direction = LEFT;
}
if (keystate[KEY_UP] && snake.direction !== DOWN) {
snake.direction = UP;
}
if (keystate[KEY_RIGHT] && snake.direction !== LEFT) {
snake.direction = RIGHT;
}
if (keystate[KEY_DOWN] && snake.direction !== UP) {
snake.direction = DOWN;
}
// each five frames update the game state.
if (frames%5 === 0) {
// pop the last element from the snake queue i.e. the
// head
var nx = snake.last.x;
var ny = snake.last.y;
// updates the position depending on the snake direction
switch (snake.direction) {
case LEFT:
nx--;
break;
case UP:
ny--;
break;
case RIGHT:
nx++;
break;
case DOWN:
ny++;
break;
}
// checks all gameover conditions
if (0 > nx || nx > grid.width-1 ||
0 > ny || ny > grid.height-1 ||
grid.get(nx, ny) === SNAKE
) {
return init();
}
// check wheter the new position are on the fruit item
if (grid.get(nx, ny) === FRUIT) {
// increment the score and sets a new fruit position
score++;
setFood();
} else {
// take out the first item from the snake queue i.e
// the tail and remove id from grid
var tail = snake.remove();
grid.set(EMPTY, tail.x, tail.y);
}
// add a snake id at the new position and append it to
// the snake queue
grid.set(SNAKE, nx, ny);
snake.insert(nx, ny);
}
}
/**
* Render the grid to the canvas.
*/
function draw() {
// calculate tile-width and -height
var tw = canvas.width/grid.width;
var th = canvas.height/grid.height;
// iterate through the grid and draw all cells
for (var x=0; x < grid.width; x++) {
for (var y=0; y < grid.height; y++) {
// sets the fillstyle depending on the id of
// each cell
switch (grid.get(x, y)) {
case EMPTY:
ctx.fillStyle = "#fff";
break;
case SNAKE:
ctx.fillStyle = "#0ff";
break;
case FRUIT:
ctx.fillStyle = "#f00";
break;
}
ctx.fillRect(x*tw, y*th, tw, th);
}
}
// changes the fillstyle once more and draws the score
// message to the canvas
ctx.fillStyle = "#000";
ctx.fillText("SCORE: " + score, 10, canvas.height-10);
}
// start and run the game
main();
</script>
</body>
</html>
EDI: here is where I'm having problems
I added a high score variable here:
/**
* Game objects
high_scores = []; // new code */
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score, /* number, keep track of the player score */
As well as here:
} else {
// take out the first item from the snake queue i.e
// the tail and remove id from grid
var tail = snake.remove();
grid.set(EMPTY, tail.x, tail.y);
high_scores = high_scores.push(score)
}
i don't get how a couple of javascript changes make the layout disappear. I've never coded in javascript that much and I dont understand why javaScript changes are affecting the layout.
I found a solution. there was an error in my syntax here
/**
* Game objects
*/
canvas, /* HTMLCanvas */
ctx, /* CanvasRenderingContext2d */
keystate, /* Object, used for keyboard inputs */
frames, /* number, used for animation */
score, /* number, keep track of the player score */
high_scores = [];

JQuery Spot of light with Sparkles, How to achieve it, is there something ready similar?

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

Categories

Resources