diff --git a/css/pokemon-types-d3.css b/css/pokemon-types-d3.css index b8fd971..518b794 100644 --- a/css/pokemon-types-d3.css +++ b/css/pokemon-types-d3.css @@ -7,7 +7,7 @@ svg circle:hover { stroke-width: 3px; } -.focused { +#focused-text { font-family: 'Open Sans Condensed', sans-serif; font-size: 10vw; text-anchor: middle; @@ -15,7 +15,7 @@ svg circle:hover { user-select: none; } -.title { +#title-text { font-family: 'Open Sans Condensed', sans-serif; font-size: 4vw; text-anchor: middle; diff --git a/src/index.ts b/src/index.ts index 8770a01..2ff320c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,14 @@ import stream from 'mithril/stream'; import * as d3 from 'd3'; import { PokemonType, INode } from './type_to_nodes'; import { updateVisualisation } from './update_visualisation'; -import { IState } from './utils'; +import { boundingDimensions, IState } from './utils'; import { forceSimulation } from './simulation'; const state: IState = { + activeTransition: false, + setActiveTransition: function(is_active: boolean) { + this.activeTransition = is_active; + }, focusedType: stream('fire'), hoveredNode: stream(undefined), setFocusedType: function(newType: PokemonType) { @@ -18,7 +22,7 @@ const state: IState = { m.redraw(); }, setHoveredNode: function(newNode?: INode) { - if (newNode === this.hoveredNode()) { + if (newNode === this.hoveredNode() || this.activeTransition) { return; } @@ -27,6 +31,28 @@ const state: IState = { } }; +function visualisationTitle(state: IState): string { + const hovered = state.hoveredNode(); + const focused = state.focusedType(); + + if (hovered === undefined) { + return ''; + } + + let attacking; + let defending; + + if (hovered.direction === 'from') { + attacking = hovered.name; + defending = focused; + } else { + attacking = focused; + defending = hovered.name; + } + + return `${attacking} gets ${hovered.multiplier}x against ${defending}`; +} + const Visualisation: m.Component<{ simulation: d3.Simulation, state: IState, @@ -36,11 +62,31 @@ const Visualisation: m.Component<{ oncreate: function({attrs: {simulation, state}, dom}) { updateVisualisation(dom, simulation, true, state); this.oldFocused = state.focusedType(); + this.domComputations(state, dom); }, onupdate: function({attrs: {simulation, state}, dom}) { const focusedUpdated = state.focusedType() !== this.oldFocused; updateVisualisation(dom, simulation, focusedUpdated, state); this.oldFocused = state.focusedType(); + this.domComputations(state, dom); + }, + domComputations: function(state: IState, dom: Element) { + this.updateFocusedText(state, dom); + this.updateTitle(state, dom); + }, + updateFocusedText: function(state: IState, dom: Element) { + const {height, width} = boundingDimensions(dom); + const el = dom.querySelector('#focused-text'); + el.setAttribute('x', (width * 0.5).toString()); + el.setAttribute('y', (height * 0.5).toString()); + el.textContent = state.focusedType(); + }, + updateTitle: function(state: IState, dom: Element) { + const {height, width} = boundingDimensions(dom); + const el = dom.querySelector('#title-text'); + el.setAttribute('x', (width * 0.5).toString()); + el.setAttribute('y', (height * 0.125).toString()); + el.textContent = visualisationTitle(state); }, view: () => { return m( @@ -49,6 +95,8 @@ const Visualisation: m.Component<{ version: '1', xmlns: 'http://www.w3.org/2000/svg', }, + m('text#focused-text', {}, 'bug'), + m('text#title-text', {}, 'bug'), ); }, }; diff --git a/src/update_visualisation.ts b/src/update_visualisation.ts index 4f458e1..b70dab9 100644 --- a/src/update_visualisation.ts +++ b/src/update_visualisation.ts @@ -1,6 +1,6 @@ import * as d3 from 'd3'; import { Pokedex } from 'pokeapi-js-wrapper'; -import { PokemonType, INode } from './type_to_nodes'; +import { INode } from './type_to_nodes'; import { boundingDimensions, nodeRadius, IState } from './utils'; import type_to_nodes from './type_to_nodes'; import { tick } from './simulation'; @@ -31,8 +31,6 @@ export async function updateVisualisation( svg.setAttribute('width', width.toString()); svg.setAttribute('height', height.toString()); - updateTitle(svg, state); - if (focusedUpdated) { const response = await pokedex.getTypeByName(state.focusedType()); const nodes: INode[] = type_to_nodes(response); @@ -43,65 +41,6 @@ export async function updateVisualisation( } updateCircles(svg, simulation, state); - updateFocused(svg, state.focusedType()); -} - - -function updateFocused(svg: Element, focused: PokemonType): void { - updateTextElement(svg, (focused as string), 'focused', 0.5, 0.5); -} - -export function visualisationTitle(state: IState): string { - const hovered = state.hoveredNode(); - const focused = state.focusedType(); - - if (hovered === undefined) { - return ''; - } - - let attacking; - let defending; - - if (hovered.direction === 'from') { - attacking = hovered.name; - defending = focused; - } else { - attacking = focused; - defending = hovered.name; - } - - return `${attacking} gets ${hovered.multiplier}x against ${defending}`; -} - -function updateTitle(svg: Element, state: IState): void { - updateTextElement(svg, visualisationTitle(state), 'title', 0.5, 0.125); -} - -function updateTextElement( - svg: Element, - text: string, - className: string, - xMultiple: number, - yMultiple: number, -): void { - const { width, height } = boundingDimensions(svg); - - const updating = d3.select(svg) - .selectAll(`.${className}`) - .data([text], d => (d as string)); - - updating - .enter() - .append('text') - .merge(updating) - .classed(className, true) - .attr('x', width * xMultiple) - .attr('y', height * yMultiple) - .text(id); - - updating - .exit() - .remove(); } function updateCircles( @@ -110,8 +49,14 @@ function updateCircles( state: IState, ): void { const { width, height } = boundingDimensions(svg); - const nodeTransition = d3.transition() - .duration(600); + const nodeTransition = d3.transition('circles') + .duration(600) + .on('start', () => { + state.setActiveTransition(true); + }) + .on('end', () => { + state.setActiveTransition(false); + }); const updatingNodes = d3.select(svg) .selectAll('circle') diff --git a/src/utils.ts b/src/utils.ts index a2f83a5..4ca4e36 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,8 +3,10 @@ import { PokemonType, INode } from './type_to_nodes'; export interface IState { focusedType: () => PokemonType; hoveredNode: () => INode | undefined; + activeTransition: boolean; setFocusedType: (newType: PokemonType) => undefined; setHoveredNode: (newNode?: INode) => undefined; + setActiveTransition: (isActive: boolean) => undefined; } const NODE_SIZE_FACTOR = 0.03;