",
35 | foreground: 'hsl(232, 77%, 39%)',
36 | background: 'hsl(40, 39%, 95%)'
37 | }, {
38 | name: "Scroll",
39 | foreground: 'hsl(0, 0%, 9%)',
40 | background: 'hsl(35, 46%, 87%)'
41 | }, {
42 | name: "Neo",
43 | foreground: 'rgb(32,255,30)',
44 | background: 'black'
45 | }]
46 |
--------------------------------------------------------------------------------
/src/lib/components/color-selector.amcss:
--------------------------------------------------------------------------------
1 | .ColorSelector {
2 | text-align: center;
3 | > li {
4 | @trait(layout: m0.25, type: 14pt bold upcase);
5 | display: inline-block;
6 | padding: 0.6rem 1.5rem 0.5rem;
7 | border: solid 1px rgba(0,0,0,0.2);
8 | border-radius: 0.25rem;
9 |
10 | cursor: pointer;
11 |
12 | &:hover {
13 | box-shadow: 0 0 4px -2px;
14 | }
15 | &.-selected {
16 | border-color: currentColor;
17 | box-shadow: 0 0 5px -1px black;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/components/color-selector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './color-selector.amcss!'
3 | import Colors from '../colors'
4 |
5 | export default class ColorSelector extends React.Component {
6 | componentWillMount() {
7 | this.colorsActions = this.context.flux.getActions('colors')
8 | this.chooseColor(Colors[0])
9 | }
10 |
11 | chooseColor(color) {
12 | this.colorsActions.chooseColor(color)
13 | }
14 |
15 | render() {
16 | return {
17 | Colors.map(o => - {o.name}
)
18 | }
19 | }
20 |
21 | getStyle(option) {
22 | return {
23 | backgroundColor: option.background,
24 | color: option.foreground
25 | }
26 | }
27 | }
28 |
29 | ColorSelector.contextTypes = {
30 | flux: React.PropTypes.object
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/components/controls.amcss:
--------------------------------------------------------------------------------
1 | .Controls {
2 | @trait (layout: p1 pb2 pt2, color: default);
3 | width: 320px;
4 | @media screen and (max-width: 600px) {
5 | width: 100%;
6 | }
7 | @media screen and (min-width: 1200px) {
8 | width: 480px;
9 | }
10 |
11 | > h2 {
12 | @trait (type: 18pt medium, layout: mt2 mb1);
13 | &:first-child {
14 | margin-top: 0;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/components/controls.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MessageEntry from './message-entry.jsx!'
3 | import FluxComponent from 'flummox/component'
4 | import TypeSelector from './type-selector.jsx!'
5 | import ColorSelector from './color-selector.jsx!'
6 | import './controls.amcss!'
7 |
8 | export default class Controls extends React.Component {
9 | render() {
10 | return
11 |
Enter your message:
12 |
13 | Pick your type pairing:
14 |
15 |
16 |
17 | Choose a colour scheme:
18 |
19 |
20 |
21 |
22 | }
23 | }
24 |
25 | Controls.contextTypes = {
26 | flux: React.PropTypes.object
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/components/frame.amcss:
--------------------------------------------------------------------------------
1 | .Frame {
2 | min-height: 100vh;
3 | @trait(flex: vertical);
4 |
5 | > header {
6 | @trait(color: inverted, type: logo, layout: p1);
7 | }
8 | > footer {
9 | @trait(flex: wrap center, color: inverted, type: footer, layout: p1);
10 | > p {
11 | @trait(layout: p0.25 pr1 pl1);
12 | text-align: center;
13 | > a {
14 | @trait(link: inline, type: unbroken);
15 | }
16 | }
17 | }
18 | > main {
19 | @trait(flex: wrap, flex-child: grow);
20 | }
21 | }
22 |
23 | .Frame-Logo {
24 | text-align: center;
25 | > span {
26 | display: inline-block;
27 | &::first-letter {
28 | font-size: 1.4em;
29 | vertical-align: text-top;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/lib/components/frame.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import FluxComponent from 'flummox/component'
3 | import './frame.amcss!'
4 |
5 | import Controls from './controls.jsx!'
6 | import Output from './output.jsx!'
7 |
8 | export default class Frame extends React.Component {
9 | render() {
10 | return
11 |
12 |
13 | TypeSlab
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/components/imgur-upload.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch'
2 |
3 | export default {
4 | uploadSingleImage(canvas) {
5 | let data = new FormData()
6 | data.append("image", canvas.toDataURL().split(',')[1])
7 | data.append("type", "base64")
8 | data.append("description", "Made with http://typeslab.com")
9 | return fetch('https://api.imgur.com/3/image', {
10 | method: 'post',
11 | body: data,
12 | headers: {
13 | "Authorization": "Client-ID dc208153560e2ef"
14 | }
15 | }).then(response => response.json())
16 | },
17 |
18 | createAlbum(imageIds, title) {
19 | let data = new FormData()
20 | data.append("ids", imageIds.join(','))
21 | data.append("title", title)
22 | return fetch('https://api.imgur.com/3/album', {
23 | method: 'post',
24 | body: data,
25 | headers: {
26 | "Authorization": "Client-ID dc208153560e2ef"
27 | }
28 | }).then(response => response.json())
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/components/message-entry.amcss:
--------------------------------------------------------------------------------
1 | .MessageEntry {
2 | > textarea {
3 | @trait(color: recessed, layout: p0.5, type: 14pt);
4 | height: 10rem;
5 | width: 100%;
6 | border: none;
7 | resize: none;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/components/message-entry.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './message-entry.amcss!'
3 |
4 | export default class MessageEntry extends React.Component {
5 | componentWillMount() {
6 | this.messageActions = this.context.flux.getActions('message')
7 | this.setValue("Whatever you write here\nwill be rendered as a slab-type\nposter!\nStart a line with an '!'\n!to use the alternate typeface\nnow go and write something\nprofound\nand share it with the world!")
8 | }
9 | handleChange(event) {
10 | this.setValue(event.target.value)
11 | }
12 | setValue(value) {
13 | this.messageActions.changeMessage(value)
14 | this.setState({value: value})
15 | }
16 |
17 | render() {
18 | return
19 |
21 | }
22 | }
23 |
24 | MessageEntry.contextTypes = {
25 | flux: React.PropTypes.object
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/components/output.amcss:
--------------------------------------------------------------------------------
1 | .Output {
2 | padding-top: 1rem;
3 | @trait (flex-child: grow);
4 | text-align: center;
5 | overflow: hidden;
6 | max-width: calc(100vw - 320px);
7 | @media screen and (max-width: 600px) {
8 | max-width: 100%;
9 | }
10 | canvas {
11 | max-width: 100%;
12 | height: auto !important;
13 | display: inline-block;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/components/output.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Surface from 'react-canvas/lib/Surface'
3 | import Layer from 'react-canvas/lib/Layer'
4 | import Text from 'react-canvas/lib/Text'
5 | import FontFace from 'react-canvas/lib/FontFace'
6 | import Share from './share.jsx!'
7 | import measureText from 'react-canvas/lib/measureText'
8 | import Typesetter from '../models/typesetter'
9 | import './output.amcss!'
10 |
11 | class Line extends React.Component {
12 | render() {
13 | return {this.props.line.line}
14 | }
15 | }
16 |
17 | export default class Output extends React.Component {
18 | constructor() {
19 | super()
20 | this.spacing = 32
21 | this.state = {lines: [], height: this.spacing}
22 | }
23 |
24 | componentDidMount() {
25 | this.setState({
26 | canvas: this.refs.surface.getDOMNode()
27 | })
28 | this.componentWillReceiveProps(this.props)
29 | }
30 |
31 | componentWillReceiveProps(newProps) {
32 | if (newProps.chosenFont) {
33 | if (!this.typesetter || newProps.chosenFont != this.props.chosenFont || newProps.mode != this.props.mode) {
34 | this.typesetter = new Typesetter(newProps.chosenFont, newProps.width, this.spacing, newProps.mode || "justified")
35 | }
36 | let result = this.typesetter.setLines(newProps.lines, newProps.chosenColor)
37 | this.setState({
38 | lines: result.sizedLines,
39 | height: result.totalHeight
40 | })
41 | }
42 | }
43 |
44 | render() {
45 | let text = 'typeslab.com',
46 | canvasWidth = this.props.width + this.spacing * 2,
47 | canvasHeight = this.state.height + this.spacing
48 | //requestAnimationFrame(_ => requestAnimationFrame(this.calculateBottomPixels.bind(this, canvasHeight)))
49 | //
50 |
51 | return
52 |
53 |
54 | {this.props.noBorder ? null : }
55 | {this.state.lines.map((line) => )}
56 | {this.props.noBorder ? null : {text}}
57 |
58 |
59 |
60 | }
61 |
62 | getBorderStyle(height) {
63 | return {
64 | borderColor: this.props.chosenColor.foreground,
65 | top: this.spacing / 2,
66 | width: this.props.width + this.spacing,
67 | left: this.spacing / 2,
68 | height: height,
69 | zIndex: 1
70 | }
71 | }
72 |
73 | getByLineStyle(text, height) {
74 | let font = FontFace('Avenir Next Condensed, Helvetica, sans-serif', null, {weight: 400}),
75 | size = 8,
76 | width = measureText(text, 9999, font, size, 15).width
77 | return {
78 | fontFace: font,
79 | fontSize: size,
80 | backgroundColor: this.props.chosenColor.background,
81 | color: this.props.chosenColor.foreground,
82 | textAlign: 'center',
83 | width: width + 6,
84 | left: this.props.width / 2 + this.spacing / 4,
85 | top: height + 11,
86 | height: 16,
87 | zIndex: 3
88 | }
89 | }
90 | }
91 |
92 | Output.contextTypes = {
93 | flux: React.PropTypes.object
94 | }
95 |
--------------------------------------------------------------------------------
/src/lib/components/share.amcss:
--------------------------------------------------------------------------------
1 | .Share {
2 | @trait(layout: pt2 pb2, flex: center wrap align-center);
3 | > p {
4 | width: 100%;
5 | @trait(layout: pb0.5);
6 | > a {
7 | @trait(link: inline);
8 | }
9 | }
10 | }
11 |
12 | .ShareButton {
13 | @trait(layout: p0.5 m0.5, link: button);
14 | display: block;
15 | border: 1px solid;
16 | border-radius: 0.25rem;
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/components/share.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './share.amcss!'
3 | import ImgurUpload from './imgur-upload'
4 |
5 | export default class Share extends React.Component {
6 | constructor() {
7 | super()
8 | this.state = {}
9 | }
10 |
11 | componentWillReceiveProps(newProps) {
12 | if (newProps.message != this.props.message || newProps.color != this.props.color || newProps.font != this.props.font) {
13 | this.setState({link: undefined})
14 | }
15 | }
16 |
17 | uploadToImgur() {
18 | this.setState({uploading: true})
19 | ImgurUpload.uploadSingleImage(this.props.canvas)
20 | .then(json => {
21 | this.setState({uploading: false})
22 | if (json.success) {
23 | this.setState({link: json.data.link})
24 | } else {
25 | let message = json.data.error.match(/anonymous uploading in your country has been disabled/) ? "Sorry, IMGUR has blocked anonymous uploads from your country" : json.data.error
26 | this.setState({failed: true, failure: message})
27 | }
28 | })
29 | }
30 |
31 | saveLocally(e) {
32 | e.target.href = this.props.canvas.toDataURL()
33 | }
34 |
35 | render() {
36 | return
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/components/type-selector.amcss:
--------------------------------------------------------------------------------
1 | .TypeSelector {
2 | > li {
3 | @trait (layout: m0.25 p0.25, flex: inline align-baseline);
4 | border: solid 1px rgba(0, 0, 0, 0.2);
5 | border-radius: 0.25rem;
6 | cursor: pointer;
7 | > span {
8 | @trait (layout: p0.25);
9 | &:first-child {
10 | }
11 | }
12 |
13 | &:hover {
14 | box-shadow: 0 0 4px -2px;
15 | }
16 | &.-selected {
17 | border-color: black;
18 | box-shadow: 0 0 5px -1px;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/components/type-selector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './type-selector.amcss!'
3 |
4 | export default class TypeSelector extends React.Component {
5 | componentWillMount() {
6 | this.fontsActions = this.props.flux.getActions('fonts')
7 | this.fontsActions.loadAllFonts()
8 | }
9 |
10 | chooseFont(font) {
11 | this.fontsActions.chooseFont(font)
12 | }
13 |
14 | render() {
15 | return {
16 | this.props.loadedFonts.map(f =>
17 | -
18 | {f.main.name}
19 | {f.alt.name}
20 |
21 | )
22 | }{ this.props.stillLoading ? "Loading..." : }
23 | }
24 |
25 | getTextStyle(font) {
26 | return {
27 | fontFamily: font.name,
28 | textTransform: font.caps ? 'uppercase' : 'initial',
29 | fontWeight: font.weight,
30 | fontStyle: font.italic ? 'italic' : 'initial'
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/flux.js:
--------------------------------------------------------------------------------
1 | import { Flummox } from 'flummox'
2 | import MessageActions from './actions/message-actions'
3 | import MessageStore from './stores/message-store'
4 | import FontsActions from './actions/fonts-actions'
5 | import FontsStore from './stores/fonts-store'
6 | import ColorsActions from './actions/colors-actions'
7 | import ColorsStore from './stores/colors-store'
8 |
9 | export default class Flux extends Flummox {
10 | constructor() {
11 | super()
12 |
13 | this.createActions('message', MessageActions)
14 | this.createStore('message', MessageStore, this)
15 | this.createActions('fonts', FontsActions)
16 | this.createStore('fonts', FontsStore, this)
17 | this.createActions('colors', ColorsActions)
18 | this.createStore('colors', ColorsStore, this)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/lib/main.js:
--------------------------------------------------------------------------------
1 | import '../styles/index'
2 | import React from 'react'
3 | import App from './app.jsx!'
4 | import Flux from './flux'
5 |
6 | const flux = new Flux()
7 |
8 | React.render(React.createElement(App, {flux}), document.querySelector('main'))
9 |
--------------------------------------------------------------------------------
/src/lib/models/depth-mapper.js:
--------------------------------------------------------------------------------
1 | export default class DepthMapper {
2 | constructor(ctx, width, fontFace, text, fontSize, alignment) {
3 | this.width = width
4 | this.fontFace = fontFace
5 | this.fontSize = fontSize
6 | this.text = text
7 | this.height = Math.floor(this.fontSize * 3)
8 | ctx.font = `${this.fontFace.attributes.style} normal ${this.fontFace.attributes.weight} ${this.fontSize}pt ${this.fontFace.family}`
9 | ctx.clearRect(0, 0, this.width, this.height)
10 | ctx.fillStyle = "black"
11 | ctx.textAlign = alignment
12 | let anchor = (alignment === "center") ? this.width / 2 : 0
13 | ctx.fillText(text, anchor, this.height / 2, this.width)
14 | this.calculateDepthMap(ctx.getImageData(0, 0, this.width, this.height))
15 | }
16 |
17 | calculateDepthMap(imageData) {
18 | let pixels = new Uint32Array(imageData.data.buffer),
19 | topBuffer = new ArrayBuffer(this.width * 4),
20 | bottomBuffer = new ArrayBuffer(this.width * 4)
21 | this.topDepth = new Uint32Array(topBuffer)
22 | this.bottomDepth = new Uint32Array(bottomBuffer)
23 |
24 | for (var line = 0; line < this.height; line++) {
25 | for (var col = 0; col < this.width; col++) {
26 | if (!this.topDepth[col]) {
27 | var topCellIdx = line * this.width + col
28 | if (pixels[topCellIdx] !== 0) this.topDepth[col] = line
29 | }
30 | if (!this.bottomDepth[col]) {
31 | var bottomCellIdx = (this.height - line - 1) * this.width + col
32 | if (pixels[bottomCellIdx] !== 0) this.bottomDepth[col] = line
33 | }
34 | }
35 | }
36 | }
37 |
38 | getMinDepth(depth) {
39 | let min = this.height / 2
40 | for (var i = 0; i < depth.length; i++) {
41 | var d = depth[i]
42 | if (d > 0 && d < min) min = d
43 | }
44 | return min
45 | }
46 |
47 | getLeadingFromTop() {
48 | return this.getMinDepth(this.topDepth)
49 | }
50 |
51 | getLeadingFromBottom() {
52 | return this.getMinDepth(this.bottomDepth)
53 | }
54 |
55 | getLeading(prevMetrics) {
56 | let min = this.height / 2 + prevMetrics.height / 2
57 | for (var i = 0; i < this.topDepth.length; i++) {
58 | var d1 = this.topDepth[i], d2 = prevMetrics.bottomDepth[i]
59 | if (d1 > 0 && d2 > 0 && (d1 + d2) < min) min = d1 + d2
60 | }
61 | return min
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/models/line-metrics.js:
--------------------------------------------------------------------------------
1 | export default class LineMetrics {
2 | constructor(ctx, width, fontFace, text) {
3 | this.text = text
4 | this.width = width
5 | this.fontFace = fontFace
6 | this.fontExpr = `${this.fontFace.attributes.style} normal ${this.fontFace.attributes.weight} 18pt ${this.fontFace.family}`
7 | ctx.font = this.fontExpr
8 | let naturalWidth = ctx.measureText(text).width
9 | this.fontSize = Math.min(300, 18 * width / naturalWidth)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/models/typesetter.js:
--------------------------------------------------------------------------------
1 | import LineMetrics from './line-metrics'
2 | import DepthMapper from './depth-mapper'
3 | import FontFace from 'react-canvas/lib/FontFace'
4 |
5 | let getFontFace = (font) => {
6 | let options = {weight: font.weight}
7 | if (font.italic) options.style = 'italic'
8 | return FontFace(font.name, null, options)
9 | }
10 |
11 | const JUSTIFIED = Symbol()
12 | const UNIFORM = Symbol()
13 | export default class Typesetter {
14 | constructor(typePair, width, spacing, mode) {
15 | this.typePair = typePair
16 | this.width = width
17 | this.spacing = spacing
18 | this.depthCache = new Map()
19 | this.setupCanvas()
20 | this.mode = (mode === "uniform") ? UNIFORM : JUSTIFIED
21 | }
22 |
23 | setupCanvas() {
24 | this.canvas = document.createElement("canvas")
25 | this.canvas.width = this.width
26 | this.canvas.height = 900
27 | this.canvas.style.backgroundColor = "palegoldenrod"
28 | this.ctx = this.canvas.getContext("2d")
29 | }
30 |
31 | getMetrics(line) {
32 | let font = this.typePair.main, text = line;
33 | if (line.match(/^!/)) {
34 | font = this.typePair.alt;
35 | text = text.replace(/^!/, '');
36 | }
37 | text = font.caps ? text.toUpperCase() : text
38 | var fontFace = getFontFace(font);
39 | return new LineMetrics(this.ctx, this.width, fontFace, text)
40 | }
41 |
42 | getMinFontSize(metrics) {
43 | let min = 300
44 | metrics.forEach(m => {
45 | if (m.fontSize < min) min = m.fontSize
46 | })
47 | return min
48 | }
49 |
50 | getDepths(lines) {
51 | let lineMetrics = lines.map(line => this.getMetrics(line)),
52 | minFontSize = this.getMinFontSize(lineMetrics)
53 | return lineMetrics.map(metrics => {
54 | let fontSize = this.mode === JUSTIFIED ? metrics.fontSize : minFontSize,
55 | key = `${fontSize} ${metrics.fontExpr} ${metrics.text}`,
56 | alignment = this.mode === JUSTIFIED ? 'center' : 'left'
57 | if (!this.depthCache.has(key)) {
58 | let depth = new DepthMapper(this.ctx, this.width, metrics.fontFace, metrics.text, fontSize, alignment)
59 | this.depthCache.set(key, depth)
60 | }
61 | return this.depthCache.get(key)
62 | })
63 | }
64 |
65 | setLines(lines, chosenColor) {
66 | let lineDepths = this.getDepths(lines)
67 | let totalHeight = this.spacing
68 | let sizedLines = []
69 | for (var i = 0; i < lineDepths.length; i++) {
70 | let line = lineDepths[i], text = line.text
71 | if (i == 0) {
72 | totalHeight += line.height / 2 - line.getLeadingFromTop()
73 | } else {
74 | let prev = lineDepths[i - 1]
75 | //totalHeight += prev.height / 2 - prev.getLeadingFromBottom()
76 | //totalHeight += line.height / 2 - line.getLeadingFromTop()
77 | totalHeight += 4 + Math.max(line.height / 6, (prev.height + line.height) / 2 - line.getLeading(prev))
78 | }
79 | let modeStyles = (this.mode === JUSTIFIED) ? {
80 | width: this.width + this.spacing * 2,
81 | left: 0,
82 | textAlign: 'center'
83 | } : {
84 | width: this.width + this.spacing,
85 | left: this.spacing,
86 | textAlign: 'left'
87 | }
88 | let style = Object.assign({
89 | fontSize: line.fontSize,
90 | height: line.height,
91 | lineHeight: 0,
92 | top: totalHeight - line.fontSize,
93 | fontFace: line.fontFace,
94 | color: chosenColor.foreground,
95 | zIndex: 2
96 | },modeStyles)
97 | if (i == lineDepths.length - 1) {
98 | totalHeight += line.height / 2 - line.getLeadingFromBottom()
99 | }
100 | sizedLines.push({line: text, style})
101 | }
102 | return {totalHeight, sizedLines}
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/lib/stores/colors-store.js:
--------------------------------------------------------------------------------
1 | import { Store } from 'flummox'
2 |
3 | export default class ColorsStore extends Store {
4 | constructor(flux) {
5 | super()
6 |
7 | const colorsActions = flux.getActions('colors')
8 | this.register(colorsActions.chooseColor, this.handleChosenColor)
9 | }
10 |
11 | handleChosenColor(color) {
12 | this.setState({
13 | chosenColor: color
14 | })
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/stores/fonts-store.js:
--------------------------------------------------------------------------------
1 | import { Store } from 'flummox'
2 |
3 | export default class FontsStore extends Store {
4 | constructor(flux) {
5 | super()
6 |
7 | const fontsActions = flux.getActions('fonts')
8 | this.register(fontsActions.fontLoaded, this.handleFontLoaded)
9 | this.register(fontsActions.chooseFont, this.handleChosenFont)
10 | this.register(fontsActions.fontLoadingFinished, this.finishLoading)
11 | this.state = {
12 | loadedFonts: [],
13 | stillLoading: true
14 | }
15 | }
16 |
17 | handleFontLoaded(font) {
18 | this.setState({
19 | loadedFonts: this.state.loadedFonts.concat([font]),
20 | chosenFont: this.state.chosenFont || font
21 | })
22 | }
23 |
24 | handleChosenFont(font) {
25 | this.setState({
26 | chosenFont: font
27 | })
28 | }
29 |
30 | finishLoading() {
31 | console.log("finished!")
32 | this.setState({
33 | stillLoading: false
34 | })
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/stores/message-store.js:
--------------------------------------------------------------------------------
1 | import { Store } from 'flummox'
2 |
3 | export default class MessageStore extends Store {
4 | constructor(flux) {
5 | super()
6 |
7 | const messageActions = flux.getActions('message')
8 | this.register(messageActions.changeMessage, this.handleChangedMessage)
9 | this.register(messageActions.imageRendered, this.handleNewImage)
10 | this.state = {
11 | message: "",
12 | lines: []
13 | }
14 | }
15 |
16 | handleChangedMessage(content) {
17 | this.setState({
18 | message: content,
19 | lines: content.split("\n").filter(x => x !== "" && x !== "!")
20 | })
21 | }
22 |
23 | handleNewImage(dataUrl) {
24 | this.setState({
25 | imageUrl: dataUrl
26 | })
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/lib/themes.js:
--------------------------------------------------------------------------------
1 | export default {
2 | default: {
3 | background: 'white',
4 | text: 'black'
5 | },
6 | grey: {
7 | background: '#ddd',
8 | text: '#444'
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/type-pairings.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | main: {
4 | local: ['Avenir Next', 'Arial Black', 'sans-serif'],
5 | weight: 900,
6 | caps: true
7 | },
8 | alt: {
9 | local: ['Gill Sans', 'Georgia', 'Times New Roman', 'serif'],
10 | weight: 300,
11 | italic: true
12 | }
13 | },
14 | {
15 | main: {
16 | google: 'Sigmar One',
17 | weight: 400,
18 | caps: true
19 | },
20 | alt: {
21 | google: 'Gentium Book Basic',
22 | weight: 400,
23 | italic: true
24 | }
25 | },
26 | {
27 | main: {
28 | google: 'Raleway',
29 | weight: 900,
30 | caps: true
31 | },
32 | alt: {
33 | google: 'Raleway',
34 | weight: 200
35 | }
36 | },
37 | {
38 | main: {
39 | google: 'Alfa Slab One',
40 | weight: 400,
41 | caps: true
42 | },
43 | alt: {
44 | google: 'Raleway',
45 | weight: 200
46 | }
47 | },
48 | {
49 | main: {
50 | google: 'Gentium Book Basic',
51 | weight: 700,
52 | italic: true,
53 | caps: true
54 | },
55 | alt: {
56 | google: 'Open Sans',
57 | weight: 300
58 | }
59 | },
60 | {
61 | main: {
62 | google: 'Playfair Display',
63 | weight: 900,
64 | caps: true,
65 | italic: true
66 | },
67 | alt: {
68 | google: 'Lato',
69 | weight: 100
70 | }
71 | }
72 | ]
73 |
--------------------------------------------------------------------------------
/src/styles/core.amcss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | @trait(type: sans normal, color: default);
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 |
11 | button {
12 | background: initial;
13 | border: initial;
14 | font: inherit;
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/index.js:
--------------------------------------------------------------------------------
1 | // Inject the styles (async obvs)
2 | import './reset.amcss!'
3 | import './variables.amcss!'
4 | import './traits/colors.amcss!'
5 | import './traits/typography.amcss!'
6 | import './traits/layout.amcss!'
7 | import './traits/flex.amcss!'
8 | import './traits/link.amcss!'
9 | import './core.amcss!'
10 |
--------------------------------------------------------------------------------
/src/styles/reset.amcss:
--------------------------------------------------------------------------------
1 | /* http://meyerweb.com/eric/tools/css/reset/
2 | v2.0 | 20110126
3 | License: none (public domain)
4 | */
5 |
6 | html, body, div, span, applet, object, iframe,
7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8 | a, abbr, acronym, address, big, cite, code,
9 | del, dfn, em, img, ins, kbd, q, s, samp,
10 | small, strike, strong, sub, sup, tt, var,
11 | b, u, i, center,
12 | dl, dt, dd, ol, ul, li,
13 | fieldset, form, label, legend,
14 | table, caption, tbody, tfoot, thead, tr, th, td,
15 | article, aside, canvas, details, embed,
16 | figure, figcaption, footer, header, hgroup,
17 | menu, nav, output, ruby, section, summary,
18 | time, mark, audio, video {
19 | margin: 0;
20 | padding: 0;
21 | border: 0;
22 | font-size: 100%;
23 | font: inherit;
24 | vertical-align: baseline;
25 | }
26 | /* HTML5 display-role reset for older browsers */
27 | article, aside, details, figcaption, figure,
28 | footer, header, hgroup, menu, nav, section {
29 | display: block;
30 | }
31 | body {
32 | line-height: 1;
33 | }
34 | ol, ul {
35 | list-style: none;
36 | }
37 | blockquote, q {
38 | quotes: none;
39 | }
40 | blockquote:before, blockquote:after,
41 | q:before, q:after {
42 | content: '';
43 | content: none;
44 | }
45 | table {
46 | border-collapse: collapse;
47 | border-spacing: 0;
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/traits/colors.amcss:
--------------------------------------------------------------------------------
1 | @define-mixin color {
2 | }
3 | @define-mixin color:default {
4 | background-color: $light-background;
5 | color: $dark-background;
6 | }
7 | @define-mixin color:inverted {
8 | background-color: $dark-background;
9 | color: $light-text;
10 | }
11 |
12 | @define-mixin color:recessed {
13 | background-color: #eee;
14 | color: black;
15 | box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/traits/flex.amcss:
--------------------------------------------------------------------------------
1 | @define-mixin flex {
2 | display: flex;
3 | }
4 |
5 | @define-mixin flex:inline {
6 | display: inline-flex;
7 | }
8 |
9 | @define-mixin flex:vertical {
10 | flex-direction: column;
11 | }
12 |
13 | @define-mixin flex:wrap {
14 | flex-wrap: wrap;
15 | }
16 |
17 | @define-mixin flex:start {
18 | justify-content: start;
19 | }
20 | @define-mixin flex:center {
21 | justify-content: center;
22 | }
23 | @define-mixin flex:space-around {
24 | justify-content: space-around;
25 | }
26 |
27 | @define-mixin flex:align-start {
28 | align-items: flex-start;
29 | }
30 | @define-mixin flex:align-center {
31 | align-items: center;
32 | }
33 | @define-mixin flex:align-baseline {
34 | align-items: baseline;
35 | }
36 | @define-mixin flex:align-stretch {
37 | align-items: stretch;
38 | }
39 |
40 | @define-mixin flex-child {
41 | }
42 | @define-mixin flex-child:no-shrink {
43 | flex-shrink: 0;
44 | }
45 | @define-mixin flex-child:grow {
46 | flex-grow: 1;
47 | }
48 |
--------------------------------------------------------------------------------
/src/styles/traits/layout.amcss:
--------------------------------------------------------------------------------
1 | @define-mixin layout {
2 | }
3 | @define-mixin layout:p0.25 {
4 | padding: 0.25rem;
5 | }
6 | @define-mixin layout:p0.5 {
7 | padding: 0.5rem;
8 | }
9 | @define-mixin layout:p1 {
10 | padding: 1rem;
11 | }
12 | @define-mixin layout:p2 {
13 | padding: 2rem;
14 | }
15 |
16 | @define-mixin layout:pt0.25 {
17 | padding-top: 0.25rem;
18 | }
19 | @define-mixin layout:pr0.25 {
20 | padding-right: 0.25rem;
21 | }
22 | @define-mixin layout:pb0.25 {
23 | padding-bottom: 0.25rem;
24 | }
25 | @define-mixin layout:pl0.25 {
26 | padding-left: 0.25rem;
27 | }
28 |
29 | @define-mixin layout:pt0.5 {
30 | padding-top: 0.5rem;
31 | }
32 | @define-mixin layout:pr0.5 {
33 | padding-right: 0.5rem;
34 | }
35 | @define-mixin layout:pb0.5 {
36 | padding-bottom: 0.5rem;
37 | }
38 | @define-mixin layout:pl0.5 {
39 | padding-left: 0.5rem;
40 | }
41 |
42 | @define-mixin layout:pt1 {
43 | padding-top: 1rem;
44 | }
45 | @define-mixin layout:pr1 {
46 | padding-right: 1rem;
47 | }
48 | @define-mixin layout:pb1 {
49 | padding-bottom: 1rem;
50 | }
51 | @define-mixin layout:pl1 {
52 | padding-left: 1rem;
53 | }
54 |
55 | @define-mixin layout:pt2 {
56 | padding-top: 2rem;
57 | }
58 | @define-mixin layout:pr2 {
59 | padding-right: 2rem;
60 | }
61 | @define-mixin layout:pb2 {
62 | padding-bottom: 2rem;
63 | }
64 | @define-mixin layout:pl2 {
65 | padding-left: 2rem;
66 | }
67 |
68 | @define-mixin layout:m0.25 {
69 | margin: 0.25rem;
70 | }
71 | @define-mixin layout:m0.5 {
72 | margin: 0.5rem;
73 | }
74 | @define-mixin layout:m1 {
75 | margin: 1rem;
76 | }
77 | @define-mixin layout:m2 {
78 | margin: 2rem;
79 | }
80 |
81 | @define-mixin layout:mt0.25 {
82 | margin-top: 0.25rem;
83 | }
84 | @define-mixin layout:mr0.25 {
85 | margin-right: 0.25rem;
86 | }
87 | @define-mixin layout:mb0.25 {
88 | margin-bottom: 0.25rem;
89 | }
90 | @define-mixin layout:ml0.25 {
91 | margin-left: 0.25rem;
92 | }
93 |
94 | @define-mixin layout:mt0.5 {
95 | margin-top: 0.5rem;
96 | }
97 | @define-mixin layout:mr0.5 {
98 | margin-right: 0.5rem;
99 | }
100 | @define-mixin layout:mb0.5 {
101 | margin-bottom: 0.5rem;
102 | }
103 | @define-mixin layout:ml0.5 {
104 | margin-left: 0.5rem;
105 | }
106 |
107 | @define-mixin layout:mt1 {
108 | margin-top: 1rem;
109 | }
110 | @define-mixin layout:mr1 {
111 | margin-right: 1rem;
112 | }
113 | @define-mixin layout:mb1 {
114 | margin-bottom: 1rem;
115 | }
116 | @define-mixin layout:ml1 {
117 | margin-left: 1rem;
118 | }
119 |
120 | @define-mixin layout:mt2 {
121 | margin-top: 2rem;
122 | }
123 | @define-mixin layout:mr2 {
124 | margin-right: 2rem;
125 | }
126 | @define-mixin layout:mb2 {
127 | margin-bottom: 2rem;
128 | }
129 | @define-mixin layout:ml2 {
130 | margin-left: 2rem;
131 | }
132 |
133 |
--------------------------------------------------------------------------------
/src/styles/traits/link.amcss:
--------------------------------------------------------------------------------
1 | @define-mixin link {
2 | cursor: pointer;
3 | color: inherit;
4 | text-decoration: none;
5 | &:hover, &:active {
6 | text-decoration: initial;
7 | color: inherit;
8 | }
9 | &:visited {
10 | color: inherit;
11 | }
12 | }
13 |
14 | @define-mixin link:button {
15 | opacity: 0.75;
16 | &:hover, &:active {
17 | opacity: 1;
18 | }
19 | &:disabled {
20 | opacity: 0.25;
21 | cursor: initial;
22 | }
23 | }
24 |
25 | @define-mixin link:inline {
26 | text-decoration: underline;
27 | opacity: 0.75;
28 | &:hover, &:active {
29 | text-decoration: underline;
30 | opacity: 1;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/styles/traits/typography.amcss:
--------------------------------------------------------------------------------
1 | @define-mixin type {
2 | }
3 |
4 | @define-mixin type:14pt {
5 | font-size: 0.875rem;
6 | }
7 | @define-mixin type:16pt {
8 | font-size: 1rem;
9 | }
10 | @define-mixin type:18pt {
11 | font-size: 1.125rem;
12 | }
13 | @define-mixin type:20pt {
14 | font-size: 1.25rem;
15 | }
16 |
17 | @define-mixin type:normal {
18 | font-weight: 400;
19 | }
20 | @define-mixin type:medium {
21 | font-weight: 500;
22 | }
23 | @define-mixin type:bold {
24 | font-weight: 700;
25 | }
26 | @define-mixin type:small-caps {
27 | font-variant: small-caps;
28 | }
29 | @define-mixin type:upcase {
30 | text-transform: uppercase;
31 | }
32 | @define-mixin type:unbroken {
33 | white-space: nowrap;
34 | }
35 |
36 | @define-mixin type:sans {
37 | font-family: "Avenir Next", "Arial Black";
38 | }
39 |
40 | @define-mixin type:logo {
41 | font-family: "Avenir Next", "Arial Black";
42 | font-weight: 700;
43 | letter-spacing: 5px;
44 | text-transform: uppercase;
45 | }
46 |
47 | @define-mixin type:heading {
48 | font-family: "Avenir Next", "Arial Black";
49 | font-weight: 400;
50 | font-size: 1.2rem;
51 | }
52 |
53 | @define-mixin type:footer {
54 | font-family: "Avenir Next", "Arial Black";
55 | font-weight: 700;
56 | font-size: 0.8rem;
57 | letter-spacing: 2px;
58 | text-transform: uppercase;
59 | line-height: 1.4;
60 | }
61 |
--------------------------------------------------------------------------------
/src/styles/variables.amcss:
--------------------------------------------------------------------------------
1 | $dark-background: hsl(240,7%,29%);
2 | $light-background: white;
3 | $light-text: hsl(240,7%,99%);
4 | $borders: hsl(240,4%,40%);
5 |
--------------------------------------------------------------------------------