Custom Preview for Header Logo
This commit is contained in:
parent
69325fbe02
commit
a318b18236
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
|
@ -1,9 +1,13 @@
|
|||
/*eslint no-bitwise:0 */
|
||||
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const WIDTH = 400;
|
||||
const HEIGHT = 220;
|
||||
import {
|
||||
createPreviewComponent,
|
||||
loadImage,
|
||||
darkLightDiff,
|
||||
chooseBrighter,
|
||||
drawHeader
|
||||
} from 'wizard/lib/preview';
|
||||
|
||||
const LINE_HEIGHT = 12.0;
|
||||
|
||||
const LOREM = `
|
||||
|
@ -19,107 +23,30 @@ accumsan sapien, nec feugiat quam. Quisque non risus.
|
|||
placerat lacus vitae, lacinia nisi. Sed metus arcu, iaculis
|
||||
sit amet cursus nec, sodales at eros.`;
|
||||
|
||||
function loadImage(src) {
|
||||
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,
|
||||
export default createPreviewComponent(400, 220, {
|
||||
logo: null,
|
||||
avatar: null,
|
||||
|
||||
themeId: Ember.computed.alias('step.fieldsById.theme_id.value'),
|
||||
@observes('step.fieldsById.theme_id.value')
|
||||
themeChanged() {
|
||||
this.triggerRepaint();
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
const c = this.$('canvas')[0];
|
||||
this.ctx = c.getContext("2d");
|
||||
|
||||
Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'),
|
||||
load() {
|
||||
return Ember.RSVP.Promise.all([loadImage('/images/wizard/discourse-small.png'),
|
||||
loadImage('/images/wizard/trout.png')]).then(result => {
|
||||
this.logo = result[0];
|
||||
this.avatar = result[1];
|
||||
this.loaded = true;
|
||||
this.triggerRepaint();
|
||||
});
|
||||
},
|
||||
|
||||
@observes('themeId')
|
||||
triggerRepaint() {
|
||||
Ember.run.scheduleOnce('afterRender', this, 'repaint');
|
||||
},
|
||||
paint(ctx, colors, width, height) {
|
||||
const headerHeight = height * 0.15;
|
||||
|
||||
repaint() {
|
||||
if (!this.loaded) { return; }
|
||||
drawHeader(ctx, colors, width, headerHeight);
|
||||
|
||||
const { ctx } = this;
|
||||
const headerHeight = HEIGHT * 0.15;
|
||||
|
||||
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;
|
||||
const margin = width * 0.02;
|
||||
const avatarSize = height * 0.1;
|
||||
|
||||
// Logo
|
||||
const headerMargin = headerHeight * 0.2;
|
||||
|
@ -128,19 +55,19 @@ export default Ember.Component.extend({
|
|||
ctx.drawImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight);
|
||||
|
||||
// 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.font = "0.75em FontAwesome";
|
||||
ctx.fillText("\uf0c9", WIDTH - (avatarSize * 2) - (headerMargin * 0.5), avatarSize);
|
||||
ctx.fillText("\uf002", WIDTH - (avatarSize * 3) - (headerMargin * 0.5), avatarSize);
|
||||
ctx.fillText("\uf0c9", width - (avatarSize * 2) - (headerMargin * 0.5), avatarSize);
|
||||
ctx.fillText("\uf002", width - (avatarSize * 3) - (headerMargin * 0.5), avatarSize);
|
||||
|
||||
// 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.fillStyle = colors.primary;
|
||||
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'";
|
||||
|
||||
|
@ -148,52 +75,45 @@ export default Ember.Component.extend({
|
|||
|
||||
const lines = LOREM.split("\n");
|
||||
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);
|
||||
}
|
||||
|
||||
// Reply Button
|
||||
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.fill();
|
||||
ctx.fillStyle = chooseBrighter(colors.primary, colors.secondary);
|
||||
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
|
||||
ctx.font = "0.5em FontAwesome";
|
||||
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.fillText("\uf040", WIDTH * 0.525, line + (LINE_HEIGHT * 1.8));
|
||||
ctx.fillText("\uf040", width * 0.525, line + (LINE_HEIGHT * 1.8));
|
||||
|
||||
// Draw Timeline
|
||||
const timelineX = WIDTH * 0.8;
|
||||
const timelineX = width * 0.8;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = colors.tertiary;
|
||||
ctx.lineWidth = 0.5;
|
||||
ctx.moveTo(timelineX, HEIGHT * 0.3);
|
||||
ctx.lineTo(timelineX, HEIGHT * 0.6);
|
||||
ctx.moveTo(timelineX, height * 0.3);
|
||||
ctx.lineTo(timelineX, height * 0.6);
|
||||
ctx.stroke();
|
||||
|
||||
// Timeline
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = colors.tertiary;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.moveTo(timelineX, HEIGHT * 0.3);
|
||||
ctx.lineTo(timelineX, HEIGHT * 0.4);
|
||||
ctx.moveTo(timelineX, height * 0.3);
|
||||
ctx.lineTo(timelineX, height * 0.4);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.font = "Bold 0.5em Arial";
|
||||
ctx.fillStyle = colors.primary;
|
||||
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();
|
||||
ctx.fillText("1 / 20", timelineX + margin, (height * 0.3) + (margin * 1.5));
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -5,7 +5,28 @@ import computed from 'ember-addons/ember-computed-decorators';
|
|||
|
||||
const Wizard = Ember.Object.extend({
|
||||
@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() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{#if field.value}}
|
||||
{{component previewComponent field=field fieldClass=fieldClass}}
|
||||
{{component previewComponent field=field fieldClass=fieldClass wizard=wizard}}
|
||||
{{/if}}
|
||||
|
||||
<label class="wizard-btn wizard-btn-upload {{if uploading 'disabled'}}">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</label>
|
||||
|
||||
<div class='input-area'>
|
||||
{{component inputComponentName field=field step=step fieldClass=fieldClass}}
|
||||
{{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard}}
|
||||
</div>
|
||||
|
||||
{{#if field.errorDescription}}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
{{#wizard-step-form step=step}}
|
||||
{{#each step.fields as |field|}}
|
||||
{{wizard-field field=field step=step}}
|
||||
{{wizard-field field=field step=step wizard=wizard}}
|
||||
{{/each}}
|
||||
{{/wizard-step-form}}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue