Browse Source

add server-side image generator sample

master
Xin Wei 8 years ago
parent
commit
801caa15d3
  1. 2
      image-maker/.gitignore
  2. 31
      image-maker/README.md
  3. 8
      image-maker/firebase.json
  4. 117
      image-maker/functions/clock.js
  5. 70
      image-maker/functions/index.js
  6. 11
      image-maker/functions/package.json
  7. 67
      image-maker/functions/ray.js
  8. 23
      image-maker/functions/sparkline.js
  9. 237
      image-maker/public/index.html

2
image-maker/.gitignore

@ -0,0 +1,2 @@
.firebaserc
node_modules

31
image-maker/README.md

@ -0,0 +1,31 @@
# Image Maker
This sample shows how to create various images through Functions and serve it to the client
It uses [node-canvas](https://github.com/Automattic/node-canvas) to create a canvas environment on Node. That canvas is then used to create either a clock, sparkline chart, or sphere(s) in png format. The images are then cached on the server and sent to the client in `image/png` format.
## Setting up the sample
1. Create a Firebase Project using the [Firebase Console](https://console.firebase.google.com).
1. Clone or download this repo and open the `image-maker` directory.
1. You must have the Firebase CLI installed. If you don't have it install it with `npm install -g firebase-tools` and then configure it with `firebase login`.
1. Configure the CLI locally by using `firebase use --add` and select your project in the list.
1. Install dependencies locally by running: `cd functions; npm install;`
## Deploy and test
This sample comes with a web-based UI for testing the function. To test it out:
1. Deploy your project using `firebase deploy`
1. Open the app using `firebase open hosting:site`, this will open a browser.`
## Contributing
We'd love that you contribute to the project. Before doing so please read our [Contributor guide](../CONTRIBUTING.md).
## License
© Google, 2017. Licensed under an [Apache-2](../LICENSE) license.

8
image-maker/firebase.json

@ -0,0 +1,8 @@
{
"hosting": {
"public": "public",
"rewrites": [
{"source":"/api/**", "function":"app" }
]
}
}

117
image-maker/functions/clock.js

@ -0,0 +1,117 @@
const _ = require('lodash');
const getDefaultOpts = () => ({
strokes: {
clock: '#325FA2',
hour: '#000000',
minute: '#000000',
seconds: '#D40000'
},
fills: {
clock: '#eeeeee',
tip: '#555555',
seconds: '#D40000'
}
});
const getX = (angle) => {
return -Math.sin(angle + Math.PI)
};
const getY = (angle) => {
return Math.cos(angle + Math.PI)
};
const clock = (ctx, colorOpts) => {
const colors = _.merge({}, getDefaultOpts(), colorOpts);
let x, y, i;
const now = new Date();
ctx.clearRect(0, 0, 320, 320);
ctx.save();
// Clock background
ctx.translate(160, 160);
ctx.beginPath();
ctx.lineWidth = 14;
ctx.strokeStyle = colors.strokes.clock;
ctx.fillStyle = colors.fills.clock;
ctx.arc(0, 0, 142, 0, Math.PI * 2, true);
ctx.stroke();
ctx.fill();
// Hour marks
ctx.lineWidth = 8;
ctx.strokeStyle = colors.strokes.hour;
for (i = 0; i < 12; i++) {
x = getX(Math.PI / 6 * i);
y = getY(Math.PI / 6 * i);
ctx.beginPath();
ctx.moveTo(x * 100, y * 100);
ctx.lineTo(x * 125, y * 125);
ctx.stroke();
}
// Minute marks
ctx.lineWidth = 5;
ctx.strokeStyle = colors.strokes.minute;
for (i = 0; i < 60; i++) {
if (i % 5 !== 0) {
x = getX(Math.PI / 30 * i);
y = getY(Math.PI / 30 * i);
ctx.beginPath();
ctx.moveTo(x * 117, y * 117);
ctx.lineTo(x * 125, y * 125);
ctx.stroke();
}
}
const sec = now.getSeconds();
const min = now.getMinutes();
const hr = now.getHours() % 12;
ctx.fillStyle = 'black';
// Write hours
x = getX(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec);
y = getY(hr * (Math.PI / 6) + (Math.PI / 360) * min + (Math.PI / 21600) * sec);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(x * -20, y * -20);
ctx.lineTo(x * 80, y * 80);
ctx.stroke();
// Write minutes
x = getX((Math.PI / 30) * min + (Math.PI / 1800) * sec);
y = getY((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 10;
ctx.beginPath();
ctx.moveTo(x * -28, y * -28);
ctx.lineTo(x * 112, y * 112);
ctx.stroke();
// Write seconds
x = getX(sec * Math.PI / 30);
y = getY(sec * Math.PI / 30);
ctx.strokeStyle = colors.strokes.seconds;
ctx.fillStyle = colors.fills.seconds;
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(x * -30, y * -30);
ctx.lineTo(x * 83, y * 83);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
ctx.beginPath();
ctx.arc(x * 95, y * 95, 10, 0, Math.PI * 2, true);
ctx.stroke();
ctx.fillStyle = colors.fills.tip;
ctx.arc(0, 0, 3, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
}
module.exports = clock;

70
image-maker/functions/index.js

@ -0,0 +1,70 @@
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const functions = require('firebase-functions');
const app = require('express')();
const Canvas = require('canvas-prebuilt');
const _ = require('lodash');
const clock = require('./clock');
const spark = require('./sparkline');
const ray = require('./ray');
app.get('/api/ray', (req, res) => {
const tracers = JSON.parse(req.query.tracers);
if (!_.isArray(tracers) ||
!_.every(tracers, (depth) => typeof depth === 'number')) {
//invalid format
res.status(422);
res.end();
}
const canvas = new Canvas(243 * tracers.length, 243);
const ctx = canvas.getContext('2d');
for (var i=0; i<tracers.length; i++) {
ray(Math.round(27/tracers[i]), 81, ctx, { x: 243, y: 0 });
}
res.set('Cache-Control', 'public, max-age=60, s-maxage=31536000');
res.writeHead(200, { 'Content-Type': 'image/png' })
canvas.pngStream().pipe(res);
});
app.get('/api/clock', (req, res) => {
const colorOpts = req.query;
const canvas = new Canvas(320, 320)
const ctx = canvas.getContext('2d')
clock(ctx, colorOpts);
res.set('Cache-Control', 'public, max-age=60, s-maxage=31536000');
res.writeHead(200, { 'Content-Type': 'image/png' })
canvas.pngStream().pipe(res);
});
app.get('/api/spark', (req, res) => {
const dataSeries = JSON.parse(req.query.series);
const colorOpts = req.query.colorOpts || {};
if (!_.isArray(dataSeries) || !_.every(dataSeries, (num) => typeof num === 'number')) {
//invalid format
res.status(422);
res.end();
}
const canvas = new Canvas(320, 100);
var ctx = canvas.getContext('2d');
spark(ctx, dataSeries, colorOpts);
res.set('Cache-Control', 'public, max-age=60, s-maxage=31536000');
res.writeHead(200, { 'Content-Type': 'image/png' })
canvas.pngStream().pipe(res);
});
exports.app = functions.https.onRequest(app);

11
image-maker/functions/package.json

@ -0,0 +1,11 @@
{
"name": "functions-image-maker",
"description": "Sample functions that generate images on the backend",
"dependencies": {
"canvas-prebuilt": "1.6.5-prerelease.1",
"express": "4.15.2",
"firebase-admin": "4.2.1",
"firebase-functions": "0.5.6",
"lodash": "4.17.4"
}
}

67
image-maker/functions/ray.js

@ -0,0 +1,67 @@
const render = (min, max, ctx, posObject) => {
ctx.fillStyle = getPointColor(122, 122);
ctx.fillRect(0, 0, 240, 240);
renderLevel(min, max, 0, ctx);
ctx.translate(posObject.x, posObject.y);
};
const renderLevel = (minimumLevel, level, y, ctx) => {
let x;
for (x = 0; x < 243 / level; ++x) {
drawBlock(x, y, level, ctx);
}
for (x = 0; x < 243 / level; x += 3) {
drawBlock(x, y + 1, level, ctx);
drawBlock(x + 2, y + 1, level, ctx);
}
for (x = 0; x < 243 / level; ++x) {
drawBlock(x, y + 2, level, ctx);
}
if ((y += 3) >= 243 / level) {
y = 0;
level /= 3;
}
if (level >= minimumLevel) {
renderLevel(minimumLevel, level, y, ctx);
}
};
const drawBlock = (x, y, level, ctx) => {
ctx.fillStyle = getPointColor(
x * level + (level - 1) / 2,
y * level + (level - 1) / 2
);
ctx.fillRect(
x * level,
y * level,
level,
level
);
};
const getPointColor = (x, y) => {
x = x / 121.5 - 1;
y = -y / 121.5 + 1;
const x2y2 = x * x + y * y;
if (x2y2 > 1) {
return '#000';
}
const root = Math.sqrt(1 - x2y2);
const x3d = x * 0.7071067812 + root / 2 - y / 2;
const y3d = x * 0.7071067812 - root / 2 + y / 2;
const z3d = 0.7071067812 * root + 0.7071067812 * y;
let brightness = -x / 2 + root * 0.7071067812 + y / 2;
if (brightness < 0) brightness = 0;
const r = Math.round(brightness * 127.5 * (1 - y3d));
const g = Math.round(brightness * 127.5 * (x3d + 1));
const b = Math.round(brightness * 127.5 * (z3d + 1));
return 'rgb(' + r + ', ' + g + ', ' + b + ')';
}
module.exports = render;

23
image-maker/functions/sparkline.js

@ -0,0 +1,23 @@
const spark = (ctx, data, opts) => {
const len = data.length;
const pad = 1;
const width = ctx.canvas.width;
const height = ctx.canvas.height;
const barWidth = width / len;
const max = Math.max.apply(null, data);
ctx.fillStyle = opts.barFill || 'rgba(0,0,255,0.5)';
ctx.strokeStyle = opts.lineStroke || 'red';
ctx.lineWidth = 1;
data.forEach((n, i) => {
const x = i * barWidth + pad;
const y = height * (n / max);
ctx.lineTo(x, height - y);
ctx.fillRect(x, height, barWidth - pad, -y);
})
ctx.stroke();
};
module.exports = spark;

237
image-maker/public/index.html

@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Demonstrates how to dynamically generate images on the server">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Maker Samples</title>
<!-- Material Design Lite -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.blue_grey-orange.min.css">
<script defer src="https://code.getmdl.io/1.1.3/material.min.js"></script>
<style>
.mdl-card {
width: 400px;
margin: 10px;
}
section.row {
display: flex;
flex-direction: row;
width: 100%;
justify-content: center;
margin: 10px 0;
align-items: baseline;
}
.inline {
display: inline-block;
margin: 0 5px;
}
.image-container {
text-align: center;
display: none;
}
#ray-tracing {
min-width: 400px;
width: auto;
}
</style>
</head>
<body>
<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header">
<!-- Header section containing title -->
<header class="mdl-layout__header mdl-color-text--white mdl-color--light-blue-700">
<div class="mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid">
<div class="mdl-layout__header-row mdl-cell mdl-cell--12-col mdl-cell--12-col-tablet mdl-cell--8-col-desktop">
<h3>Image Maker Samples</h3>
</div>
</div>
</header>
<main class="mdl-layout__content mdl-color--grey-100">
<div class="mdl-cell--12-col mdl-cell--12-col-tablet mdl-grid" style="align-items: baseline;">
<section class='row'>
<div class="mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Clock</h2>
</div>
<div class="mdl-card__supporting-text" id='clock-directions'>Generates a PNG of a clock based on the following parameters.</div>
<div class="mdl-card__supporting-text image-container" id='clock-png'></div>
<div class="mdl-card__supporting-text">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='clock-stroke' value='#325FA2' />
<label class="mdl-textfield__label" for="clock-stroke">Clock Stroke</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='clock-fill' value='#eeeeee' />
<label class="mdl-textfield__label" for="clock-fill">Clock Fill</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='hour-stroke' value='#000000' />
<label class="mdl-textfield__label" for="hour-stroke">Hour Hand Stroke</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='minute-stroke' value='#000000' />
<label class="mdl-textfield__label" for="minute-stroke">Minute Hand Stroke</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='seconds-stroke' value='#D40000' />
<label class="mdl-textfield__label" for="seconds-stroke">Seconds Hand Stroke</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='seconds-fill' value='#D40000' />
<label class="mdl-textfield__label" for="seconds-fill">Seconds Hand Fill</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='seconds-tip' value='#555555' />
<label class="mdl-textfield__label" for="seconds-tip">Seconds Tip Fill</label>
</div>
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" onClick={generateClock()}>
Generate Image
</a>
</div>
</div>
<div class="mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Sparkline Chart</h2>
</div>
<div class="mdl-card__supporting-text" id='spark-directions'>Generates a PNG of a Sparkline Chart. Input an array of integers as a sample data series.</div>
<div class="mdl-card__supporting-text image-container" id='spark-png'></div>
<div class="mdl-card__supporting-text">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:100%">
<textarea class="mdl-textfield__input" type="text" rows= "1" id="sampleSeries">[1, 2, 4, 5, 10, 4, 2, 5, 4, 3, 3, 2]</textarea>
<label class="mdl-textfield__label" for='sampleSeries'>Sample Data Series</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='line-stroke' value='#D40000' />
<label class="mdl-textfield__label" for="line-stroke">Line Stroke</label>
</div>
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label inline" style="width:46%">
<input class="mdl-textfield__input" type='color' id='bar-fill' value='#EEEEEE' />
<label class="mdl-textfield__label" for="bar-fill">Bar Fill</label>
</div>
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" onClick={generateSparkChart()}>
Generate Image
</a>
</div>
</div>
</section>
<section class='row'>
<div class="mdl-card mdl-shadow--2dp" id='ray-tracing'>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Ray Tracing</h2>
</div>
<div class="mdl-card__supporting-text" id='ray-directions'>Generates a PNG of a sphere using a recursive ray tracing algorithm. </div>
<div class='image-container' id='ray-png'></div>
<div id="ray-sections">
<div class="mdl-card__supporting-text mdl-card--border raySection">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:100%">
<input class="mdl-textfield__input" type="number" value="27" min="1" max="27" id="depth" />
<label class="mdl-textfield__label" for="depth">Min Depth Level</label>
</div>
</div>
<div class="mdl-card__supporting-text mdl-card--border raySection">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:100%">
<input class="mdl-textfield__input" type="number" value="1" min="1" max="27" id="depth" />
<label class="mdl-textfield__label" for="depth">Min Depth Level</label>
</div>
</div>
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" onClick={generateTracers()}>
Generate Image
</a>
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" onClick={addTracer()}>
Add New Tracer
</a>
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" onClick={removeTracer()}>
Remove Last Tracer
</a>
</div>
</div>
</section>
</div>
</main>
</div>
<script
src="https://code.jquery.com/jquery-2.2.4.min.js"
integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
crossorigin="anonymous">
</script>
<script>
function updateImage(divId, imageUrl) {
$(`#${divId}-png`).html(`<div class='loading'>generating...</div><img src='${imageUrl}'/>`);
$(`#${divId}-png img`).on('load', function() {
$(`#${divId}-png .loading`).remove();
});
$(`#${divId}-png`).css('display','block');
$(`#${divId}-directions`).css('display','hidden');
}
function addTracer() {
$('#ray-sections').append(`<div class="mdl-card__supporting-text mdl-card--border raySection">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width:100%">
<input class="mdl-textfield__input" type="number" value="13" min="1" max="27" id="depth" />
<label class="mdl-textfield__label" for="depth">Min Depth Level</label>
</div>
</div>`);
componentHandler.upgradeElements(document.getElementById('ray-sections'));
}
function removeTracer() {
if ($('#ray-sections').children().length > 1) {
$('#ray-sections').children().last().remove();
}
}
function generateTracers() {
const tracers = $('.raySection').map(function(ind, elm) {
return parseInt($(elm).find("input#depth").val())
});
// construct data obj
const data = {
tracers: JSON.stringify(tracers.toArray())
};
// construct URL
const url = `/api/ray?${$.param(data)}`;
// update the img-container
updateImage('ray', url);
}
function generateSparkChart() {
// construct data obj
const data = {
series: $('#sampleSeries').val(),
colorOpts: {
lineStroke: $('#line-stroke').val(),
barFill: $('#bar-fill').val()
}
};
// construct URL
const url = `/api/spark?${$.param(data)}`;
// update the img-container
updateImage('spark', url);
}
function generateClock() {
// construct data obj
const data = {
strokes: {
clock: $('#clock-stroke').val(),
minute: $('#hour-stroke').val(),
hour: $('#minute-stroke').val(),
seconds: $('#seconds-stroke').val()
},
fills: {
clock: $('#clock-fill').val(),
tip: $('#seconds-tip').val(),
seconds: $('#seconds-fill').val()
}
};
// construct URL
const url = `/api/clock?${$.param(data)}`;
// update the img-container
updateImage('clock', url);
}
</script>
</body>
</html>
Loading…
Cancel
Save