A Pen by Jan Ziemba on CodePen.
Created
May 18, 2017 12:29
-
-
Save janziemba/9b271c28c6e10fc339da0e3c75aa92d2 to your computer and use it in GitHub Desktop.
Fabric.js Restaurant reservation system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="container-fluid text-center"> | |
<h1><a href="http://fabricjs.com/" target="_blank">Fabric.js</a> Restaurant reservation system</h1> | |
<p class="text-muted">There is no need to use jQuery or Bootstrap at all, it's used only to show modals and make buttons look better. I have also used <a href="https://refreshless.com/nouislider/" target="_blank">noUiSlider</a> for time selection.</p> | |
<div class="form-group admin-menu"> | |
<div class="row"> | |
<div class="col-sm-2 col-sm-offset-3 form-group"> | |
<label>Width (px)</label> | |
<input type="number" id="width" class="form-control" value="302" /> | |
</div> | |
<div class="col-sm-2 form-group"> | |
<label>Height (px)</label> | |
<input type="number" id="height" class="form-control" value="812" /> | |
</div> | |
<div class="col-sm-2 form-group"> | |
<label> </label> | |
<br /> | |
<button class="btn btn-primary">Save</button> | |
</div> | |
</div> | |
<div class="btn-group"> | |
<button class="btn btn-primary rectangle">+ ▯ Table</button> | |
<button class="btn btn-primary circle">+ ◯ Table</button> | |
<button class="btn btn-primary triangle">+ △ Table</button> | |
<button class="btn btn-primary chair">+ Chair</button> | |
<button class="btn btn-primary bar">+ Bar</button> | |
<button class="btn btn-default wall">+ Wall</button> | |
<button class="btn btn-danger remove">Remove</button> | |
<button class="btn btn-warning customer-mode">Customer mode</button> | |
</div> | |
</div> | |
<div class="form-group customer-menu" style="display: none;"> | |
<div class="btn-group"> | |
<button class="btn btn-success submit">Submit reservation</button> | |
<button class="btn btn-warning admin-mode">Admin mode</button> | |
</div> | |
<br /> | |
<br /> | |
<div id="slider"></div> | |
<div id="slider-value"></div> | |
</div> | |
<canvas id="canvas" width="302" height="812"></canvas> | |
</div> | |
<div class="modal fade" id="modal" tabindex="-1" role="dialog"> | |
<div class="modal-dialog" role="document"> | |
<div class="modal-content"> | |
<div class="modal-body text-center"> | |
<p id="modal-table-id"></p> | |
</div> | |
<div class="modal-footer"> | |
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button> | |
</div> | |
</div> | |
</div> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let canvas | |
let number | |
const grid = 30 | |
const backgroundColor = '#f8f8f8' | |
const lineStroke = '#ebebeb' | |
const tableFill = 'rgba(150, 111, 51, 0.7)' | |
const tableStroke = '#694d23' | |
const tableShadow = 'rgba(0, 0, 0, 0.4) 3px 3px 7px' | |
const chairFill = 'rgba(67, 42, 4, 0.7)' | |
const chairStroke = '#32230b' | |
const chairShadow = 'rgba(0, 0, 0, 0.4) 3px 3px 7px' | |
const barFill = 'rgba(0, 93, 127, 0.7)' | |
const barStroke = '#003e54' | |
const barShadow = 'rgba(0, 0, 0, 0.4) 3px 3px 7px' | |
const barText = 'Bar' | |
const wallFill = 'rgba(136, 136, 136, 0.7)' | |
const wallStroke = '#686868' | |
const wallShadow = 'rgba(0, 0, 0, 0.4) 5px 5px 20px' | |
let widthEl = document.getElementById('width') | |
let heightEl = document.getElementById('height') | |
let canvasEl = document.getElementById('canvas') | |
function initCanvas() { | |
if (canvas) { | |
canvas.clear() | |
canvas.dispose() | |
} | |
canvas = new fabric.Canvas('canvas') | |
number = 1 | |
canvas.backgroundColor = backgroundColor | |
for (let i = 0; i < (canvas.height / grid); i++) { | |
const lineX = new fabric.Line([ 0, i * grid, canvas.height, i * grid], { | |
stroke: lineStroke, | |
selectable: false, | |
type: 'line' | |
}) | |
const lineY = new fabric.Line([ i * grid, 0, i * grid, canvas.height], { | |
stroke: lineStroke, | |
selectable: false, | |
type: 'line' | |
}) | |
sendLinesToBack() | |
canvas.add(lineX) | |
canvas.add(lineY) | |
} | |
canvas.on('object:moving', function(e) { | |
snapToGrid(e.target) | |
}) | |
canvas.on('object:scaling', function(e) { | |
if (e.target.scaleX > 5) { | |
e.target.scaleX = 5 | |
} | |
if (e.target.scaleY > 5) { | |
e.target.scaleY = 5 | |
} | |
if (!e.target.strokeWidthUnscaled && e.target.strokeWidth) { | |
e.target.strokeWidthUnscaled = e.target.strokeWidth | |
} | |
if (e.target.strokeWidthUnscaled) { | |
e.target.strokeWidth = e.target.strokeWidthUnscaled / e.target.scaleX | |
if (e.target.strokeWidth === e.target.strokeWidthUnscaled) { | |
e.target.strokeWidth = e.target.strokeWidthUnscaled / e.target.scaleY | |
} | |
} | |
}) | |
canvas.on('object:modified', function(e) { | |
e.target.scaleX = e.target.scaleX >= 0.25 ? (Math.round(e.target.scaleX * 2) / 2) : 0.5 | |
e.target.scaleY = e.target.scaleY >= 0.25 ? (Math.round(e.target.scaleY * 2) / 2) : 0.5 | |
snapToGrid(e.target) | |
if (e.target.type === 'table') { | |
canvas.bringToFront(e.target) | |
} | |
else { | |
canvas.sendToBack(e.target) | |
} | |
sendLinesToBack() | |
}) | |
canvas.observe('object:moving', function (e) { | |
checkBoudningBox(e) | |
}) | |
canvas.observe('object:rotating', function (e) { | |
checkBoudningBox(e) | |
}) | |
canvas.observe('object:scaling', function (e) { | |
checkBoudningBox(e) | |
}) | |
} | |
initCanvas() | |
function resizeCanvas() { | |
widthEl = document.getElementById('width') | |
heightEl = document.getElementById('height') | |
canvasEl.width = widthEl.value ? widthEl.value : 302 | |
canvasEl.height = heightEl.value ? heightEl.value : 812 | |
const canvasContainerEl = document.querySelectorAll('.canvas-container')[0] | |
canvasContainerEl.style.width = canvasEl.width | |
canvasContainerEl.style.height = canvasEl.height | |
} | |
resizeCanvas() | |
widthEl.addEventListener('change', () => { | |
resizeCanvas() | |
initCanvas() | |
addDefaultObjects() | |
}) | |
heightEl.addEventListener('change', () => { | |
resizeCanvas() | |
initCanvas() | |
addDefaultObjects() | |
}) | |
function generateId() { | |
return Math.random().toString(36).substr(2, 8) | |
} | |
function addRect(left, top, width, height) { | |
const id = generateId() | |
const o = new fabric.Rect({ | |
width: width, | |
height: height, | |
fill: tableFill, | |
stroke: tableStroke, | |
strokeWidth: 2, | |
shadow: tableShadow, | |
originX: 'center', | |
originY: 'center', | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true | |
}) | |
const t = new fabric.IText(number.toString(), { | |
fontFamily: 'Calibri', | |
fontSize: 14, | |
fill: '#fff', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'center' | |
}) | |
const g = new fabric.Group([o, t], { | |
left: left, | |
top: top, | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'table', | |
id: id, | |
number: number | |
}) | |
canvas.add(g) | |
number++ | |
return g | |
} | |
function addCircle(left, top, radius) { | |
const id = generateId() | |
const o = new fabric.Circle({ | |
radius: radius, | |
fill: tableFill, | |
stroke: tableStroke, | |
strokeWidth: 2, | |
shadow: tableShadow, | |
originX: 'center', | |
originY: 'center', | |
centeredRotation: true | |
}) | |
const t = new fabric.IText(number.toString(), { | |
fontFamily: 'Calibri', | |
fontSize: 14, | |
fill: '#fff', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'center' | |
}) | |
const g = new fabric.Group([o, t], { | |
left: left, | |
top: top, | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'table', | |
id: id, | |
number: number | |
}) | |
canvas.add(g) | |
number++ | |
return g | |
} | |
function addTriangle(left, top, radius) { | |
const id = generateId() | |
const o = new fabric.Triangle({ | |
radius: radius, | |
fill: tableFill, | |
stroke: tableStroke, | |
strokeWidth: 2, | |
shadow: tableShadow, | |
originX: 'center', | |
originY: 'center', | |
centeredRotation: true | |
}) | |
const t = new fabric.IText(number.toString(), { | |
fontFamily: 'Calibri', | |
fontSize: 14, | |
fill: '#fff', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'center' | |
}) | |
const g = new fabric.Group([o, t], { | |
left: left, | |
top: top, | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'table', | |
id: id, | |
number: number | |
}) | |
canvas.add(g) | |
number++ | |
return g | |
} | |
function addChair(left, top, width, height) { | |
const o = new fabric.Rect({ | |
left: left, | |
top: top, | |
width: 30, | |
height: 30, | |
fill: chairFill, | |
stroke: chairStroke, | |
strokeWidth: 2, | |
shadow: chairShadow, | |
originX: 'left', | |
originY: 'top', | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'chair', | |
id: generateId() | |
}) | |
canvas.add(o) | |
return o | |
} | |
function addBar(left, top, width, height) { | |
const o = new fabric.Rect({ | |
width: width, | |
height: height, | |
fill: barFill, | |
stroke: barStroke, | |
strokeWidth: 2, | |
shadow: barShadow, | |
originX: 'center', | |
originY: 'center', | |
type: 'bar', | |
id: generateId() | |
}) | |
const t = new fabric.IText(barText, { | |
fontFamily: 'Calibri', | |
fontSize: 14, | |
fill: '#fff', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'center' | |
}) | |
const g = new fabric.Group([o, t], { | |
left: left, | |
top: top, | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'bar' | |
}) | |
canvas.add(g) | |
return g | |
} | |
function addWall(left, top, width, height) { | |
const o = new fabric.Rect({ | |
left: left, | |
top: top, | |
width: width, | |
height: height, | |
fill: wallFill, | |
stroke: wallStroke, | |
strokeWidth: 2, | |
shadow: wallShadow, | |
originX: 'left', | |
originY: 'top', | |
centeredRotation: true, | |
snapAngle: 45, | |
selectable: true, | |
type: 'wall', | |
id: generateId() | |
}) | |
canvas.add(o) | |
return o | |
} | |
function snapToGrid(target) { | |
target.set({ | |
left: Math.round(target.left / (grid / 2)) * grid / 2, | |
top: Math.round(target.top / (grid / 2)) * grid / 2 | |
}) | |
} | |
function checkBoudningBox(e) { | |
const obj = e.target | |
if (!obj) { | |
return | |
} | |
obj.setCoords() | |
const objBoundingBox = obj.getBoundingRect() | |
if (objBoundingBox.top < 0) { | |
obj.set('top', 0) | |
obj.setCoords() | |
} | |
if (objBoundingBox.left > canvas.width - objBoundingBox.width) { | |
obj.set('left', canvas.width - objBoundingBox.width) | |
obj.setCoords() | |
} | |
if (objBoundingBox.top > canvas.height - objBoundingBox.height) { | |
obj.set('top', canvas.height - objBoundingBox.height) | |
obj.setCoords() | |
} | |
if (objBoundingBox.left < 0) { | |
obj.set('left', 0) | |
obj.setCoords() | |
} | |
} | |
function sendLinesToBack() { | |
canvas.getObjects().map(o => { | |
if (o.type === 'line') { | |
canvas.sendToBack(o) | |
} | |
}) | |
} | |
document.querySelectorAll('.rectangle')[0].addEventListener('click', function() { | |
const o = addRect(0, 0, 60, 60) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.circle')[0].addEventListener('click', function() { | |
const o = addCircle(0, 0, 30) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.triangle')[0].addEventListener('click', function() { | |
const o = addTriangle(0, 0, 30) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.chair')[0].addEventListener('click', function() { | |
const o = addChair(0, 0) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.bar')[0].addEventListener('click', function() { | |
const o = addBar(0, 0, 180, 60) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.wall')[0].addEventListener('click', function() { | |
const o = addWall(0, 0, 60, 180) | |
canvas.setActiveObject(o) | |
}) | |
document.querySelectorAll('.remove')[0].addEventListener('click', function() { | |
const o = canvas.getActiveObject() | |
if (o) { | |
o.remove() | |
canvas.remove(o) | |
canvas.discardActiveObject() | |
canvas.renderAll() | |
} | |
}) | |
document.querySelectorAll('.customer-mode')[0].addEventListener('click', function() { | |
canvas.getObjects().map(o => { | |
o.hasControls = false | |
o.lockMovementX = true | |
o.lockMovementY = true | |
if (o.type === 'chair' || o.type === 'bar' || o.type === 'wall') { | |
o.selectable = false | |
} | |
o.borderColor = '#38A62E' | |
o.borderScaleFactor = 2.5 | |
}) | |
canvas.selection = false | |
canvas.hoverCursor = 'pointer' | |
canvas.discardActiveObject() | |
canvas.renderAll() | |
document.querySelectorAll('.admin-menu')[0].style.display = 'none' | |
document.querySelectorAll('.customer-menu')[0].style.display = 'block' | |
}) | |
document.querySelectorAll('.admin-mode')[0].addEventListener('click', function() { | |
canvas.getObjects().map(o => { | |
o.hasControls = true | |
o.lockMovementX = false | |
o.lockMovementY = false | |
if (o.type === 'chair' || o.type === 'bar' || o.type === 'wall') { | |
o.selectable = true | |
} | |
o.borderColor = 'rgba(102, 153, 255, 0.75)' | |
o.borderScaleFactor = 1 | |
}) | |
canvas.selection = true | |
canvas.hoverCursor = 'move' | |
canvas.discardActiveObject() | |
canvas.renderAll() | |
document.querySelectorAll('.admin-menu')[0].style.display = 'block' | |
document.querySelectorAll('.customer-menu')[0].style.display = 'none' | |
}) | |
function formatTime(val) { | |
const hours = Math.floor(val / 60) | |
const minutes = val % 60 | |
const englishHours = hours > 12 ? hours - 12 : hours | |
const normal = hours + ':' + minutes + (minutes === 0 ? '0' : '') | |
const english = englishHours + ':' + minutes + (minutes === 0 ? '0' : '') + ' ' + (hours > 12 ? 'PM' : 'AM') | |
return normal + ' (' + english + ')' | |
} | |
document.querySelectorAll('.submit')[0].addEventListener('click', function() { | |
const obj = canvas.getActiveObject() | |
$('#modal').modal('show') | |
let modalText = 'You have not selected anything' | |
if (obj) { | |
modalText = 'You have selected table ' + obj.number + ', time: ' + formatTime(slider.noUiSlider.get()) | |
} | |
document.querySelectorAll('#modal-table-id')[0].innerHTML = modalText | |
}) | |
const slider = document.getElementById('slider') | |
noUiSlider.create(slider, { | |
start: 1200, | |
step: 15, | |
connect: 'lower', | |
range: { | |
min: 0, | |
max: 1425 | |
} | |
}) | |
const sliderValue = document.getElementById('slider-value') | |
slider.noUiSlider.on('update', function(values, handle) { | |
sliderValue.innerHTML = formatTime(values[handle]) | |
}) | |
function addDefaultObjects() { | |
addChair(15, 105) | |
addChair(15, 135) | |
addChair(75, 105) | |
addChair(75, 135) | |
addChair(225, 75) | |
addChair(255, 75) | |
addChair(225, 135) | |
addChair(255, 135) | |
addChair(225, 195) | |
addChair(255, 195) | |
addChair(225, 255) | |
addChair(255, 255) | |
addChair(15, 195) | |
addChair(45, 195) | |
addChair(15, 255) | |
addChair(45, 255) | |
addChair(15, 315) | |
addChair(45, 315) | |
addChair(15, 375) | |
addChair(45, 375) | |
addChair(225, 315) | |
addChair(255, 315) | |
addChair(225, 375) | |
addChair(255, 375) | |
addChair(15, 435) | |
addChair(15, 495) | |
addChair(15, 555) | |
addChair(15, 615) | |
addChair(225, 615) | |
addChair(255, 615) | |
addChair(195, 495) | |
addChair(195, 525) | |
addChair(255, 495) | |
addChair(255, 525) | |
addChair(225, 675) | |
addChair(255, 675) | |
addRect(30, 90, 60, 90) | |
addRect(210, 90, 90, 60) | |
addRect(210, 210, 90, 60) | |
addRect(0, 210, 90, 60) | |
addRect(0, 330, 90, 60) | |
addRect(210, 330, 90, 60) | |
addRect(0, 450, 60, 60) | |
addRect(0, 570, 60, 60) | |
addRect(210, 480, 60, 90) | |
addRect(210, 630, 90, 60) | |
addBar(120, 0, 180, 60) | |
addWall(120, 510, 60, 60) | |
} | |
addDefaultObjects() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.11/fabric.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> | |
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> | |
<script src="https://cdn.rawgit.com/leongersen/noUiSlider/master/distribute/nouislider.min.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
margin-top: 30px; | |
margin-bottom: 30px; | |
background: #f2f2f2; | |
} | |
.canvas-container { | |
margin-right: auto; | |
margin-left: auto; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" /> | |
<link href="https://cdn.rawgit.com/leongersen/noUiSlider/master/distribute/nouislider.min.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment