Custom Preview for Header Logo

This commit is contained in:
Robin Ward 2016-09-16 16:12:56 -04:00
parent 69325fbe02
commit a318b18236
7 changed files with 298 additions and 121 deletions

View File

@ -0,0 +1,76 @@
import { observes } from 'ember-addons/ember-computed-decorators';
import {
createPreviewComponent,
loadImage,
drawHeader,
darkLightDiff
} from 'wizard/lib/preview';
export default createPreviewComponent(400, 100, {
image: null,
@observes('field.value')
imageChanged() {
this.reload();
},
load() {
return loadImage(this.get('field.value')).then(image => {
this.image = image;
});
},
paint(ctx, colors, width, height) {
const headerHeight = height / 2;
drawHeader(ctx, colors, width, headerHeight);
const image = this.image;
const headerMargin = headerHeight * 0.2;
const imageHeight = headerHeight - (headerMargin * 2);
const ratio = imageHeight / image.height;
ctx.drawImage(image, headerMargin, headerMargin, image.width * ratio, imageHeight);
const categoriesSize = width / 3.8;
const badgeHeight = categoriesSize * 0.25;
ctx.beginPath();
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 90, -65);
ctx.rect(headerMargin, headerHeight + headerMargin, categoriesSize, badgeHeight);
ctx.fill();
const fontSize = Math.round(badgeHeight * 0.5);
ctx.font = `${fontSize}px 'Arial'`;
ctx.fillStyle = colors.primary;
ctx.fillText("all categories", headerMargin * 1.5, headerHeight + (headerMargin * 1.5) + fontSize);
ctx.font = "0.9em 'FontAwesome'";
ctx.fillStyle = colors.primary;
ctx.fillText("\uf0da", categoriesSize - (headerMargin / 4), headerHeight + (headerMargin * 1.6) + fontSize);
// pills
ctx.beginPath();
ctx.fillStyle = colors.quaternary;
ctx.rect((headerMargin * 2)+ categoriesSize, headerHeight + headerMargin, categoriesSize * 0.55, badgeHeight);
ctx.fill();
ctx.font = `${fontSize}px 'Arial'`;
ctx.fillStyle = colors.secondary;
let x = (headerMargin * 3.0) + categoriesSize;
ctx.fillText("Latest", x, headerHeight + (headerMargin * 1.5) + fontSize);
ctx.fillStyle = colors.primary;
x += categoriesSize * 0.6;
ctx.fillText("New", x, headerHeight + (headerMargin * 1.5) + fontSize);
x += categoriesSize * 0.4;
ctx.fillText("Unread", x, headerHeight + (headerMargin * 1.5) + fontSize);
x += categoriesSize * 0.6;
ctx.fillText("Top", x, headerHeight + (headerMargin * 1.5) + fontSize);
}
});

View File

@ -1,9 +1,13 @@
/*eslint no-bitwise:0 */
import { observes } from 'ember-addons/ember-computed-decorators'; import { observes } from 'ember-addons/ember-computed-decorators';
const WIDTH = 400; import {
const HEIGHT = 220; createPreviewComponent,
loadImage,
darkLightDiff,
chooseBrighter,
drawHeader
} from 'wizard/lib/preview';
const LINE_HEIGHT = 12.0; const LINE_HEIGHT = 12.0;
const LOREM = ` const LOREM = `
@ -19,107 +23,30 @@ accumsan sapien, nec feugiat quam. Quisque non risus.
placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis
sit amet cursus nec, sodales at eros.`; sit amet cursus nec, sodales at eros.`;
function loadImage(src) { export default createPreviewComponent(400, 220, {
const img = new Image();
img.src = src;
return new Ember.RSVP.Promise(resolve => img.onload = () => resolve(img));
};
function parseColor(color) {
const m = color.match(/^#([0-9a-f]{6})$/i);
if (m) {
const c = m[1];
return [ parseInt(c.substr(0,2),16), parseInt(c.substr(2,2),16), parseInt(c.substr(4,2),16) ];
}
return [0, 0, 0];
}
function brightness(color) {
return (color[0] * 0.299) + (color[1] * 0.587) + (color[2] * 0.114);
}
function lighten(color, percent) {
return '#' +
((0|(1<<8) + color[0] + (256 - color[0]) * percent / 100).toString(16)).substr(1) +
((0|(1<<8) + color[1] + (256 - color[1]) * percent / 100).toString(16)).substr(1) +
((0|(1<<8) + color[2] + (256 - color[2]) * percent / 100).toString(16)).substr(1);
}
function chooseBrighter(primary, secondary) {
const primaryCol = parseColor(primary);
const secondaryCol = parseColor(secondary);
return brightness(primaryCol) < brightness(secondaryCol) ? secondary : primary;
}
function darkLightDiff(adjusted, comparison, lightness, darkness) {
const adjustedCol = parseColor(adjusted);
const comparisonCol = parseColor(comparison);
return lighten(adjustedCol, (brightness(adjustedCol) < brightness(comparisonCol)) ?
lightness : darkness);
}
export default Ember.Component.extend({
ctx: null,
width: WIDTH,
height: HEIGHT,
loaded: false,
logo: null, logo: null,
avatar: null,
themeId: Ember.computed.alias('step.fieldsById.theme_id.value'), @observes('step.fieldsById.theme_id.value')
themeChanged() {
this.triggerRepaint();
},
didInsertElement() { load() {
this._super(); return Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'),
const c = this.$('canvas')[0];
this.ctx = c.getContext("2d");
Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'),
loadImage('/images/wizard/trout.png')]).then(result => { loadImage('/images/wizard/trout.png')]).then(result => {
this.logo = result[0]; this.logo = result[0];
this.avatar = result[1]; this.avatar = result[1];
this.loaded = true;
this.triggerRepaint();
}); });
}, },
@observes('themeId') paint(ctx, colors, width, height) {
triggerRepaint() { const headerHeight = height * 0.15;
Ember.run.scheduleOnce('afterRender', this, 'repaint');
},
repaint() { drawHeader(ctx, colors, width, headerHeight);
if (!this.loaded) { return; }
const { ctx } = this; const margin = width * 0.02;
const headerHeight = HEIGHT * 0.15; const avatarSize = height * 0.1;
const themeId = this.get('themeId');
const choices = this.get('step.fieldsById.theme_id.choices');
if (!choices) { return; }
const option = choices.findProperty('id', themeId);
const colors = option.data.colors;
if (!colors) { return; }
ctx.fillStyle = colors.secondary;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Header area
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, WIDTH, headerHeight);
ctx.fillStyle = colors.header_background;
ctx.shadowColor = "rgba(0, 0, 0, 0.25)";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 2;
ctx.fill();
ctx.restore();
const margin = WIDTH * 0.02;
const avatarSize = HEIGHT * 0.1;
// Logo // Logo
const headerMargin = headerHeight * 0.2; const headerMargin = headerHeight * 0.2;
@ -128,19 +55,19 @@ export default Ember.Component.extend({
ctx.drawImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight); ctx.drawImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight);
// Top right menu // Top right menu
ctx.drawImage(this.avatar, WIDTH - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize); ctx.drawImage(this.avatar, width - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize);
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55); ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 45, 55);
ctx.font = "0.75em FontAwesome"; ctx.font = "0.75em FontAwesome";
ctx.fillText("\uf0c9", WIDTH - (avatarSize * 2) - (headerMargin * 0.5), avatarSize); ctx.fillText("\uf0c9", width - (avatarSize * 2) - (headerMargin * 0.5), avatarSize);
ctx.fillText("\uf002", WIDTH - (avatarSize * 3) - (headerMargin * 0.5), avatarSize); ctx.fillText("\uf002", width - (avatarSize * 3) - (headerMargin * 0.5), avatarSize);
// Draw a fake topic // Draw a fake topic
ctx.drawImage(this.avatar, margin, headerHeight + (HEIGHT * 0.17), avatarSize, avatarSize); ctx.drawImage(this.avatar, margin, headerHeight + (height * 0.17), avatarSize, avatarSize);
ctx.beginPath(); ctx.beginPath();
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.font = "bold 0.75em 'Arial'"; ctx.font = "bold 0.75em 'Arial'";
ctx.fillText("Welcome to Discourse", margin, (HEIGHT * 0.25)); ctx.fillText("Welcome to Discourse", margin, (height * 0.25));
ctx.font = "0.5em 'Arial'"; ctx.font = "0.5em 'Arial'";
@ -148,52 +75,45 @@ export default Ember.Component.extend({
const lines = LOREM.split("\n"); const lines = LOREM.split("\n");
for (let i=0; i<10; i++) { for (let i=0; i<10; i++) {
line = (HEIGHT * 0.3) + (i * LINE_HEIGHT); line = (height * 0.3) + (i * LINE_HEIGHT);
ctx.fillText(lines[i], margin + avatarSize + margin, line); ctx.fillText(lines[i], margin + avatarSize + margin, line);
} }
// Reply Button // Reply Button
ctx.beginPath(); ctx.beginPath();
ctx.rect(WIDTH * 0.57, line + LINE_HEIGHT, WIDTH * 0.1, HEIGHT * 0.07); ctx.rect(width * 0.57, line + LINE_HEIGHT, width * 0.1, height * 0.07);
ctx.fillStyle = colors.tertiary; ctx.fillStyle = colors.tertiary;
ctx.fill(); ctx.fill();
ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary); ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary);
ctx.font = "8px 'Arial'"; ctx.font = "8px 'Arial'";
ctx.fillText("Reply", WIDTH * 0.595, line + (LINE_HEIGHT * 1.8)); ctx.fillText("Reply", width * 0.595, line + (LINE_HEIGHT * 1.8));
// Icons // Icons
ctx.font = "0.5em FontAwesome"; ctx.font = "0.5em FontAwesome";
ctx.fillStyle = colors.love; ctx.fillStyle = colors.love;
ctx.fillText("\uf004", WIDTH * 0.48, line + (LINE_HEIGHT * 1.8)); ctx.fillText("\uf004", width * 0.48, line + (LINE_HEIGHT * 1.8));
ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 65, 55); ctx.fillStyle = darkLightDiff(colors.primary, colors.secondary, 65, 55);
ctx.fillText("\uf040", WIDTH * 0.525, line + (LINE_HEIGHT * 1.8)); ctx.fillText("\uf040", width * 0.525, line + (LINE_HEIGHT * 1.8));
// Draw Timeline // Draw Timeline
const timelineX = WIDTH * 0.8; const timelineX = width * 0.8;
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = colors.tertiary; ctx.strokeStyle = colors.tertiary;
ctx.lineWidth = 0.5; ctx.lineWidth = 0.5;
ctx.moveTo(timelineX, HEIGHT * 0.3); ctx.moveTo(timelineX, height * 0.3);
ctx.lineTo(timelineX, HEIGHT * 0.6); ctx.lineTo(timelineX, height * 0.6);
ctx.stroke(); ctx.stroke();
// Timeline // Timeline
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = colors.tertiary; ctx.strokeStyle = colors.tertiary;
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.moveTo(timelineX, HEIGHT * 0.3); ctx.moveTo(timelineX, height * 0.3);
ctx.lineTo(timelineX, HEIGHT * 0.4); ctx.lineTo(timelineX, height * 0.4);
ctx.stroke(); ctx.stroke();
ctx.font = "Bold 0.5em Arial"; ctx.font = "Bold 0.5em Arial";
ctx.fillStyle = colors.primary; ctx.fillStyle = colors.primary;
ctx.fillText("1 / 20", timelineX + margin, (HEIGHT * 0.3) + (margin * 1.5)); ctx.fillText("1 / 20", timelineX + margin, (height * 0.3) + (margin * 1.5));
// draw border
ctx.beginPath();
ctx.strokeStyle='rgba(0, 0, 0, 0.2)';
ctx.rect(0, 0, WIDTH, HEIGHT);
ctx.stroke();
} }
}); });

View File

@ -0,0 +1,160 @@
/*eslint no-bitwise:0 */
export function createPreviewComponent(width, height, obj) {
return Ember.Component.extend({
layoutName: 'components/theme-preview',
width,
height,
ctx: null,
loaded: false,
didInsertElement() {
this._super();
const c = this.$('canvas')[0];
this.ctx = c.getContext("2d");
this.reload();
},
reload() {
this.load().then(() => {
this.loaded = true;
this.triggerRepaint();
});
},
triggerRepaint() {
Ember.run.scheduleOnce('afterRender', this, 'repaint');
},
repaint() {
if (!this.loaded) { return false; }
const colors = this.get('wizard').getCurrentColors();
if (!colors) { return; }
const { ctx } = this;
ctx.fillStyle = colors.secondary;
ctx.fillRect(0, 0, width, height);
this.paint(ctx, colors, this.width, this.height);
// draw border
ctx.beginPath();
ctx.strokeStyle='rgba(0, 0, 0, 0.2)';
ctx.rect(0, 0, width, height);
ctx.stroke();
}
}, obj);
}
export function loadImage(src) {
const img = new Image();
img.src = src;
return new Ember.RSVP.Promise(resolve => img.onload = () => resolve(img));
};
export function parseColor(color) {
const m = color.match(/^#([0-9a-f]{6})$/i);
if (m) {
const c = m[1];
return [ parseInt(c.substr(0,2),16), parseInt(c.substr(2,2),16), parseInt(c.substr(4,2),16) ];
}
return [0, 0, 0];
}
export function brightness(color) {
return (color[0] * 0.299) + (color[1] * 0.587) + (color[2] * 0.114);
}
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
function hue2rgb(p, q, t) {
if (t < 0) { t += 1; }
if (t > 1) { t -= 1; }
if (t < 1/6) { return p + (q - p) * 6 * t; }
if (t < 1/2) { return q; }
if (t < 2/3) { return p + (q - p) * (2/3 - t) * 6; }
return p;
}
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [r * 255, g * 255, b * 255];
}
export function lighten(color, percent) {
const hsl = rgbToHsl(color[0], color[1], color[2]);
const scale = percent / 100.0;
const diff = scale > 0 ? 1.0 - hsl[2] : hsl[2];
hsl[2] = hsl[2] + diff * scale;
color = hslToRgb(hsl[0], hsl[1], hsl[2]);
return '#' +
((0|(1<<8) + color[0]).toString(16)).substr(1) +
((0|(1<<8) + color[1]).toString(16)).substr(1) +
((0|(1<<8) + color[2]).toString(16)).substr(1);
}
export function chooseBrighter(primary, secondary) {
const primaryCol = parseColor(primary);
const secondaryCol = parseColor(secondary);
return brightness(primaryCol) < brightness(secondaryCol) ? secondary : primary;
}
export function darkLightDiff(adjusted, comparison, lightness, darkness) {
const adjustedCol = parseColor(adjusted);
const comparisonCol = parseColor(comparison);
return lighten(adjustedCol, (brightness(adjustedCol) < brightness(comparisonCol)) ?
lightness : darkness);
}
export function drawHeader(ctx, colors, width, height) {
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, width, height);
ctx.fillStyle = colors.header_background;
ctx.shadowColor = "rgba(0, 0, 0, 0.25)";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 2;
ctx.fill();
ctx.restore();
}

View File

@ -5,7 +5,28 @@ import computed from 'ember-addons/ember-computed-decorators';
const Wizard = Ember.Object.extend({ const Wizard = Ember.Object.extend({
@computed('steps.length') @computed('steps.length')
totalSteps: length => length totalSteps: length => length,
// A bit clunky, but get the current colors from the appropriate step
getCurrentColors() {
const colorStep = this.get('steps').findProperty('id', 'colors');
if (!colorStep) { return; }
const themeChoice = colorStep.get('fieldsById.theme_id');
if (!themeChoice) { return; }
const themeId = themeChoice.get('value');
if (!themeId) { return; }
const choices = themeChoice.get('choices');
if (!choices) { return; }
const option = choices.findProperty('id', themeId);
if (!option) { return; }
return option.data.colors;
}
}); });
export function findWizard() { export function findWizard() {

View File

@ -1,5 +1,5 @@
{{#if field.value}} {{#if field.value}}
{{component previewComponent field=field fieldClass=fieldClass}} {{component previewComponent field=field fieldClass=fieldClass wizard=wizard}}
{{/if}} {{/if}}
<label class="wizard-btn wizard-btn-upload {{if uploading 'disabled'}}"> <label class="wizard-btn wizard-btn-upload {{if uploading 'disabled'}}">

View File

@ -7,7 +7,7 @@
</label> </label>
<div class='input-area'> <div class='input-area'>
{{component inputComponentName field=field step=step fieldClass=fieldClass}} {{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard}}
</div> </div>
{{#if field.errorDescription}} {{#if field.errorDescription}}

View File

@ -9,7 +9,7 @@
{{#wizard-step-form step=step}} {{#wizard-step-form step=step}}
{{#each step.fields as |field|}} {{#each step.fields as |field|}}
{{wizard-field field=field step=step}} {{wizard-field field=field step=step wizard=wizard}}
{{/each}} {{/each}}
{{/wizard-step-form}} {{/wizard-step-form}}
</div> </div>