Skip to content

Commit

Permalink
Merge pull request #55 from bigbluebutton/tldraw-poll
Browse files Browse the repository at this point in the history
Add basic tldraw poll shape rendering
  • Loading branch information
antobinary authored Oct 4, 2024
2 parents 68a8693 + a0ecea9 commit c473e6f
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 0 deletions.
6 changes: 6 additions & 0 deletions bbb_presentation_video/events/tldraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class PropsData(StyleData, total=False):
text: str
verticalAlign: str
w: float
question: str
numResponders: int
numRespondents: int
questionType: str
questionText: str
answers: List[Dict[str, Any]]


class ShapeData(TypedDict, total=False):
Expand Down
4 changes: 4 additions & 0 deletions bbb_presentation_video/renderer/tldraw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
HighlighterShape,
LineShape,
OvalGeoShape,
PollShape,
RectangleGeoShape,
RectangleShape,
RhombusGeoShape,
Expand All @@ -87,6 +88,7 @@
from bbb_presentation_video.renderer.tldraw.shape.frame import finalize_frame
from bbb_presentation_video.renderer.tldraw.shape.highlighter import finalize_highlight
from bbb_presentation_video.renderer.tldraw.shape.line import finalize_line
from bbb_presentation_video.renderer.tldraw.shape.poll import finalize_poll
from bbb_presentation_video.renderer.tldraw.shape.rectangle import finalize_rectangle
from bbb_presentation_video.renderer.tldraw.shape.sticky import finalize_sticky
from bbb_presentation_video.renderer.tldraw.shape.sticky_v2 import finalize_sticky_v2
Expand Down Expand Up @@ -310,6 +312,8 @@ def finalize_shapes(
finalize_line(ctx, id, shape)
elif isinstance(shape, OvalGeoShape):
finalize_oval(ctx, id, shape)
elif isinstance(shape, PollShape):
finalize_poll(ctx, id, shape)
elif isinstance(shape, RectangleShape):
finalize_rectangle(ctx, id, shape)
elif isinstance(shape, RectangleGeoShape):
Expand Down
44 changes: 44 additions & 0 deletions bbb_presentation_video/renderer/tldraw/shape/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,47 @@ def update_from_data(self, data: ShapeData) -> None:
self.spline = SplineType.NONE


@attr.s(order=False, slots=True, auto_attribs=True)
class PollShapeAnswer:
key: str
numVotes: int


@attr.s(order=False, slots=True, auto_attribs=True)
class PollShape(RotatableShapeProto):
question: str = ""
numResponders: int = 0
numRespondents: int = 0
questionType: str = ""
questionText: str = ""
answers: List[PollShapeAnswer] = attr.Factory(list)

def update_from_data(self, data: ShapeData) -> None:
# Poll shapes contain a prop "fill" which isn't a valid FillStyle
if "props" in data and "fill" in data["props"]:
del data["props"]["fill"]

super().update_from_data(data)

if "props" in data:
props = data["props"]
if "question" in props:
self.question = props["question"]
if "numResponders" in props:
self.numResponders = props["numResponders"]
if "numRespondents" in props:
self.numRespondents = props["numRespondents"]
if "questionType" in props:
self.questionType = props["questionType"]
if "questionText" in props:
self.questionText = props["questionText"]
if "answers" in props:
self.answers = [
PollShapeAnswer(key=answer["key"], numVotes=answer["numVotes"])
for answer in props["answers"]
]


Shape = Union[
ArrowGeoShape,
ArrowShape,
Expand Down Expand Up @@ -608,6 +649,7 @@ def update_from_data(self, data: ShapeData) -> None:
TriangleGeoShape,
TriangleShape,
XBoxGeoShape,
PollShape,
]


Expand Down Expand Up @@ -645,6 +687,8 @@ def parse_shape_from_data(data: ShapeData, bbb_version: Version) -> Shape:
return HighlighterShape.from_data(data)
elif type == "frame":
return FrameShape.from_data(data)
elif type == "poll":
return PollShape.from_data(data)
elif type == "geo":
if "geo" in data["props"]:
geo_type = GeoShape(data["props"]["geo"])
Expand Down
178 changes: 178 additions & 0 deletions bbb_presentation_video/renderer/tldraw/shape/poll.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# SPDX-FileCopyrightText: 2024 BigBlueButton Inc. and by respective authors
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

from typing import TypeVar

import cairo
from gi.repository import Pango, PangoCairo

from bbb_presentation_video.events.helpers import Color
from bbb_presentation_video.renderer.tldraw.shape import PollShape, apply_shape_rotation
from bbb_presentation_video.renderer.tldraw.utils import (
V2_COLORS,
V2_TEXT_COLOR,
ColorStyle,
)

FONT_FAMILY = "Arial"
POLL_LINE_WIDTH = 2.0
POLL_FONT_SIZE = 18
POLL_VPADDING = 12.0
POLL_HPADDING = 12.0

CairoSomeSurface = TypeVar("CairoSomeSurface", bound=cairo.Surface)


def finalize_poll(
ctx: cairo.Context[CairoSomeSurface], id: str, shape: PollShape
) -> None:
print(f"\tTldraw: Finalizing Poll: {id}")

if len(shape.answers) == 0:
return

apply_shape_rotation(ctx, shape)

width = shape.size.width
height = shape.size.height
color = V2_COLORS.get(shape.style.color, V2_COLORS[ColorStyle.BLACK])

ctx.set_line_join(cairo.LINE_JOIN_MITER)
ctx.set_line_cap(cairo.LINE_CAP_SQUARE)

# Draw the background and poll outline
half_lw = POLL_LINE_WIDTH / 2
ctx.set_line_width(POLL_LINE_WIDTH)
ctx.move_to(half_lw, half_lw)
ctx.line_to(width - half_lw, half_lw)
ctx.line_to(width - half_lw, height - half_lw)
ctx.line_to(half_lw, height - half_lw)
ctx.close_path()
ctx.set_source_rgb(*color.semi)
ctx.fill_preserve()
ctx.set_source_rgb(*color.solid)
ctx.stroke()

font = Pango.FontDescription()
font.set_family(FONT_FAMILY)
font.set_absolute_size(int(POLL_FONT_SIZE * Pango.SCALE))

# Use Pango to calculate the label width space needed
pctx = PangoCairo.create_context(ctx)
layout = Pango.Layout(pctx)
layout.set_font_description(font)

max_label_width = 0.0
max_percent_width = 0.0
for answer in shape.answers:
layout.set_text(answer.key, -1)
(label_width, _) = layout.get_pixel_size()
if label_width > max_label_width:
max_label_width = label_width
percent: str
if shape.numResponders > 0:
percent = "{}%".format(
int(float(answer.numVotes) / float(shape.numResponders) * 100)
)
else:
percent = "0%"
layout.set_text(percent, -1)
(percent_width, _) = layout.get_pixel_size()
if percent_width > max_percent_width:
max_percent_width = percent_width

max_label_width = min(max_label_width, width * 0.3)
max_percent_width = min(max_percent_width, width * 0.3)

title_height = 0.0
if shape.questionText != "":
title_height = POLL_FONT_SIZE + POLL_VPADDING

bar_height = (height - POLL_VPADDING - title_height) / len(
shape.answers
) - POLL_VPADDING
bar_width = width - 4 * POLL_HPADDING - max_label_width - max_percent_width
bar_x = 2 * POLL_HPADDING + max_label_width

# All sizes are calculated, so draw the poll
layout.set_ellipsize(Pango.EllipsizeMode.END)
if shape.questionText != "":
title_font = Pango.FontDescription()
title_font.set_family(FONT_FAMILY)
title_font.set_absolute_size(int(POLL_FONT_SIZE * Pango.SCALE))
title_font.set_weight(Pango.Weight.BOLD)
layout.set_font_description(title_font)
layout.set_width(int(width - 2 * POLL_HPADDING) * Pango.SCALE)
layout.set_text(shape.questionText, -1)
_label_width, label_height = layout.get_pixel_size()
ctx.move_to(
POLL_HPADDING,
(POLL_FONT_SIZE - label_height) / 2 + POLL_VPADDING,
)
ctx.set_source_rgb(*V2_TEXT_COLOR)
PangoCairo.show_layout(ctx, layout)
layout.set_font_description(font)

for i, answer in enumerate(shape.answers):
bar_y = (bar_height + POLL_VPADDING) * i + POLL_VPADDING + title_height
if shape.numResponders > 0:
result_ratio = float(answer.numVotes) / float(shape.numResponders)
else:
result_ratio = 0.0
percent = "{}%".format(int(result_ratio * 100))

bar_x2 = bar_x + (bar_width * result_ratio)

# Draw the bar
ctx.set_line_width(POLL_LINE_WIDTH)
ctx.move_to(bar_x + half_lw, bar_y + half_lw)
ctx.line_to(max(bar_x + half_lw, bar_x2 - half_lw), bar_y + half_lw)
ctx.line_to(
max(bar_x + half_lw, bar_x2 - half_lw), bar_y + bar_height - half_lw
)
ctx.line_to(bar_x + half_lw, bar_y + bar_height - half_lw)
ctx.close_path()
ctx.set_source_rgb(*color.solid)
ctx.fill_preserve()
ctx.stroke()

# Draw the label and percentage
ctx.set_source_rgb(*V2_TEXT_COLOR)
layout.set_width(int(max_label_width * Pango.SCALE))
layout.set_text(answer.key, -1)
label_width, label_height = layout.get_pixel_size()
ctx.move_to(
bar_x - POLL_HPADDING - label_width,
bar_y + (bar_height - label_height) / 2,
)
PangoCairo.show_layout(ctx, layout)
layout.set_width(int(max_percent_width * Pango.SCALE))
layout.set_text(percent, -1)
percent_width, percent_height = layout.get_pixel_size()
ctx.move_to(
width - POLL_HPADDING - percent_width,
bar_y + (bar_height - percent_height) / 2,
)
PangoCairo.show_layout(ctx, layout)

# Draw the result count
layout.set_ellipsize(Pango.EllipsizeMode.NONE)
layout.set_width(-1)
layout.set_text(str(answer.numVotes), -1)
votes_width, votes_height = layout.get_pixel_size()
if votes_width < (bar_x2 - bar_x - 2 * POLL_HPADDING):
# Votes fit in the bar
ctx.move_to(
bar_x + (bar_x2 - bar_x - votes_width) / 2,
bar_y + (bar_height - votes_height) / 2,
)
ctx.set_source_rgb(*color.semi)
PangoCairo.show_layout(ctx, layout)
else:
# Votes do not fit in the bar, so put them after
ctx.move_to(bar_x2 + POLL_HPADDING, bar_y + (bar_height - votes_height) / 2)
ctx.set_source_rgb(*V2_TEXT_COLOR)
PangoCairo.show_layout(ctx, layout)
86 changes: 86 additions & 0 deletions bbb_presentation_video/renderer/tldraw/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,92 @@ class ColorStyle(Enum):
]
)

V2_TEXT_COLOR: Color = Color.from_int(0x000000)


@attr.s(order=False, slots=True, auto_attribs=True)
class V2Color:
solid: Color
semi: Color
pattern: Color
highlight: Color


V2_COLORS: Dict[ColorStyle, V2Color] = {
ColorStyle.BLACK: V2Color(
solid=Color.from_int(0x1D1D1D),
semi=Color.from_int(0xE8E8E8),
pattern=Color.from_int(0x494949),
highlight=Color.from_int(0xFDDD00),
),
ColorStyle.BLUE: V2Color(
solid=Color.from_int(0x4263EB),
semi=Color.from_int(0xDCE1F8),
pattern=Color.from_int(0x6681EE),
highlight=Color.from_int(0x10ACFF),
),
ColorStyle.GREEN: V2Color(
solid=Color.from_int(0x099268),
semi=Color.from_int(0xD3E9E3),
pattern=Color.from_int(0x39A785),
highlight=Color.from_int(0x00FFC8),
),
ColorStyle.GREY: V2Color(
solid=Color.from_int(0xADB5BD),
semi=Color.from_int(0xECEEF0),
pattern=Color.from_int(0xBCC3C9),
highlight=Color.from_int(0xCBE7F1),
),
ColorStyle.LIGHT_BLUE: V2Color(
solid=Color.from_int(0x4DABF7),
semi=Color.from_int(0xDDEDFA),
pattern=Color.from_int(0x6FBBF8),
highlight=Color.from_int(0x00F4FF),
),
ColorStyle.LIGHT_GREEN: V2Color(
solid=Color.from_int(0x40C057),
semi=Color.from_int(0xDBF0E0),
pattern=Color.from_int(0x65CB78),
highlight=Color.from_int(0x65F641),
),
ColorStyle.LIGHT_RED: V2Color(
solid=Color.from_int(0xFF8787),
semi=Color.from_int(0xF4DADB),
pattern=Color.from_int(0xFE9E9E),
highlight=Color.from_int(0xFF7FA3),
),
ColorStyle.LIGHT_VIOLET: V2Color(
solid=Color.from_int(0xE599F7),
semi=Color.from_int(0xF5EAFA),
pattern=Color.from_int(0xE9ACF8),
highlight=Color.from_int(0xFF88FF),
),
ColorStyle.ORANGE: V2Color(
solid=Color.from_int(0xF76707),
semi=Color.from_int(0xF8E2D4),
pattern=Color.from_int(0xF78438),
highlight=Color.from_int(0xFFA500),
),
ColorStyle.RED: V2Color(
solid=Color.from_int(0xE03131),
semi=Color.from_int(0xF4DADB),
pattern=Color.from_int(0xE55959),
highlight=Color.from_int(0xFF636E),
),
ColorStyle.VIOLET: V2Color(
solid=Color.from_int(0xAE3EC9),
semi=Color.from_int(0xECDCF2),
pattern=Color.from_int(0xBD63D3),
highlight=Color.from_int(0xC77CFF),
),
ColorStyle.YELLOW: V2Color(
solid=Color.from_int(0xFFC078),
semi=Color.from_int(0xF9F0E6),
pattern=Color.from_int(0xFECB92),
highlight=Color.from_int(0xFDDD00),
),
}


class DashStyle(Enum):
DRAW: str = "draw"
Expand Down
Loading

0 comments on commit c473e6f

Please sign in to comment.