Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 159 additions & 134 deletions stl/geometry/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,109 +10,81 @@ import (
"github.com/github/gh-skyline/types"
)

// Common configuration for rendered elements
type renderConfig struct {
startX float64
startY float64
startZ float64
voxelScale float64
depth float64
}
const (
baseWidthVoxelResolution = 2000 // Number of voxels across the skyline face
voxelDepth = 1.0 // Distance to come out of face

// TextConfig holds parameters for text rendering
type textRenderConfig struct {
renderConfig
text string
contextWidth int
contextHeight int
fontSize float64
}
logoScale = 0.4 // Percent
logoTopOffset = 0.15 // Percent
logoLeftOffset = 0.03 // Percent

// ImageConfig holds parameters for image rendering
type imageRenderConfig struct {
renderConfig
imagePath string
height float64
}
usernameFontSize = 120.0
usernameJustification = "left" // "left", "center", "right"
usernameLeftOffset = 0.1 // Percent

const (
imagePosition = 0.025
usernameOffset = -0.01
yearPosition = 0.77

defaultContextWidth = 800
defaultContextHeight = 200
textVoxelSize = 1.0
textDepthOffset = 2.0
frontEmbedDepth = 1.5

usernameContextWidth = 1000
usernameContextHeight = 200
usernameFontSize = 48.0
usernameZOffset = 0.7

yearContextWidth = 800
yearContextHeight = 200
yearFontSize = 56.0
yearZOffset = 0.4

defaultImageHeight = 9.0
defaultImageScale = 0.8
imageLeftMargin = 10.0
yearFontSize = 100.0
yearJustification = "right" // "left", "center", "right"
yearLeftOffset = 0.97 // Percent
)

// Create3DText generates 3D text geometry for the username and year.
func Create3DText(username string, year string, innerWidth, baseHeight float64) ([]types.Triangle, error) {
func Create3DText(username string, year string, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
if username == "" {
username = "anonymous"
}

usernameConfig := textRenderConfig{
renderConfig: renderConfig{
startX: innerWidth * usernameOffset,
startY: -textDepthOffset / 2,
startZ: baseHeight * usernameZOffset,
voxelScale: textVoxelSize,
depth: frontEmbedDepth,
},
text: username,
contextWidth: usernameContextWidth,
contextHeight: usernameContextHeight,
fontSize: usernameFontSize,
}

yearConfig := textRenderConfig{
renderConfig: renderConfig{
startX: innerWidth * yearPosition,
startY: -textDepthOffset / 2,
startZ: baseHeight * yearZOffset,
voxelScale: textVoxelSize * 0.75,
depth: frontEmbedDepth,
},
text: year,
contextWidth: yearContextWidth,
contextHeight: yearContextHeight,
fontSize: yearFontSize,
}

usernameTriangles, err := renderText(usernameConfig)
usernameTriangles, err := renderText(
username,
usernameJustification,
usernameLeftOffset,
usernameFontSize,
baseWidth,
baseHeight,
)
if err != nil {
return nil, err
}

yearTriangles, err := renderText(yearConfig)
yearTriangles, err := renderText(
year,
yearJustification,
yearLeftOffset,
yearFontSize,
baseWidth,
baseHeight,
)
if err != nil {
return nil, err
}

return append(usernameTriangles, yearTriangles...), nil
}

// renderText generates 3D geometry for the given text configuration.
func renderText(config textRenderConfig) ([]types.Triangle, error) {
dc := gg.NewContext(config.contextWidth, config.contextHeight)
// renderText places text on the face of a skyline, offset from the left and vertically-aligned.
// The function takes the text to be displayed, offset from left, and font size.
// It returns an array of types.Triangle.
//
// Parameters:
//
// text (string): The text to be displayed on the skyline's front face.
// leftOffsetPercent (float64): The percentage distance from the left to start displaying the text.
// fontSize (float64): How large to make the text. Note: It scales with the baseWidthVoxelResolution.
//
// Returns:
//
// ([]types.Triangle, error): A slice of triangles representing text.
func renderText(text string, justification string, leftOffsetPercent float64, fontSize float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
// Create a rendering context for the face of the skyline
faceWidthRes := baseWidthVoxelResolution
faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth)

// Create image representing the skyline face
dc := gg.NewContext(faceWidthRes, faceHeightRes)
dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(1, 1, 1)

// Get temporary font file
// Load font into context
fontPath, cleanup, err := writeTempFont(PrimaryFont)
if err != nil {
// Try fallback font
Expand All @@ -121,31 +93,42 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) {
return nil, errors.New(errors.IOError, "failed to load any fonts", err)
}
}

if err := dc.LoadFontFace(fontPath, config.fontSize); err != nil {
if err := dc.LoadFontFace(fontPath, fontSize); err != nil {
return nil, errors.New(errors.IOError, "failed to load font", err)
}

dc.SetRGB(0, 0, 0)
dc.Clear()
dc.SetRGB(1, 1, 1)
dc.DrawStringAnchored(config.text, float64(config.contextWidth)/8, float64(config.contextHeight)/2, 0.0, 0.5)

// Draw text on image at desired location
var triangles []types.Triangle

for y := 0; y < config.contextHeight; y++ {
for x := 0; x < config.contextWidth; x++ {
// Convert justification to a number
var justificationPercent float64
switch justification {
case "center":
justificationPercent = 0.5
case "right":
justificationPercent = 1.0
default:
justificationPercent = 0.0
}

dc.DrawStringAnchored(
text,
float64(faceWidthRes)*leftOffsetPercent, // Offset from right
float64(faceHeightRes)*0.5, // Offset from top
justificationPercent, // Justification (0.0=left, 0.5=center, 1.0=right)
0.5, // Vertically aligned
)

// Convert context image pixels into voxels
for x := 0; x < faceWidthRes; x++ {
for y := 0; y < faceHeightRes; y++ {
if isPixelActive(dc, x, y) {
xPos := config.startX + float64(x)*config.voxelScale/8
zPos := config.startZ - float64(y)*config.voxelScale/8

voxel, err := CreateCube(
xPos,
config.startY,
zPos,
config.voxelScale/2,
config.depth,
config.voxelScale/2,
voxel, err := createVoxelOnFace(
float64(x),
float64(y),
voxelDepth,
baseWidth,
baseHeight,
)
if err != nil {
return nil, errors.New(errors.STLError, "failed to create cube", err)
Expand All @@ -161,34 +144,78 @@ func renderText(config textRenderConfig) ([]types.Triangle, error) {
return triangles, nil
}

// createVoxelOnFace creates a voxel on the face of a skyline by generating a cube at the specified coordinates.
// The function takes in the x, y coordinates and height.
// It returns a slice of types.Triangle representing the cube and an error if the cube creation fails.
//
// Parameters:
//
// x (float64): The x-coordinate on the skyline face (left to right).
// y (float64): The y-coordinate on the skyline face (top to bottom).
// height (float64): Distance coming out of the face.
//
// Returns:
//
// ([]types.Triangle, error): A slice of triangles representing the cube and an error if any.
func createVoxelOnFace(x float64, y float64, height float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
// Mapping resolution
xResolution := float64(baseWidthVoxelResolution)
yResolution := xResolution * baseHeight / baseWidth

// Pixel size
voxelSize := 1.0

// Scale coordinate to face resolution
x = (x / xResolution) * baseWidth
y = (y / yResolution) * baseHeight
voxelSizeX := (voxelSize / xResolution) * baseWidth
voxelSizeY := (voxelSize / yResolution) * baseHeight

cube, err := CreateCube(
// Location (from top left corner of skyline face)
x, // x - Left to right
-height, // y - Negative comes out of face. Positive goes into face.
-voxelSizeY-y, // z - Bottom to top

// Size
voxelSizeX, // x length - left to right from specified point
height, // thickness - distance coming out of face
voxelSizeY, // y length - bottom to top from specified point
)

return cube, err
}

// GenerateImageGeometry creates 3D geometry from the embedded logo image.
func GenerateImageGeometry(innerWidth, baseHeight float64) ([]types.Triangle, error) {
func GenerateImageGeometry(baseWidth float64, baseHeight float64) ([]types.Triangle, error) {
// Get temporary image file
imgPath, cleanup, err := getEmbeddedImage()
if err != nil {
return nil, err
}

config := imageRenderConfig{
renderConfig: renderConfig{
startX: innerWidth * imagePosition,
startY: -frontEmbedDepth / 2.0,
startZ: -0.85 * baseHeight,
voxelScale: defaultImageScale,
depth: frontEmbedDepth,
},
imagePath: imgPath,
height: defaultImageHeight,
}

defer cleanup()

return renderImage(config)
return renderImage(
imgPath,
logoScale,
voxelDepth,
logoLeftOffset,
logoTopOffset,
baseWidth,
baseHeight,
)
}

// renderImage generates 3D geometry for the given image configuration.
func renderImage(config imageRenderConfig) ([]types.Triangle, error) {
reader, err := os.Open(config.imagePath)
func renderImage(filePath string, scale float64, height float64, leftOffsetPercent float64, topOffsetPercent float64, baseWidth float64, baseHeight float64) ([]types.Triangle, error) {

// Get voxel resolution of base face
faceWidthRes := baseWidthVoxelResolution
faceHeightRes := int(float64(faceWidthRes) * baseHeight / baseWidth)

// Load image from file
reader, err := os.Open(filePath)
if err != nil {
return nil, errors.New(errors.IOError, "failed to open image", err)
}
Expand All @@ -199,34 +226,32 @@ func renderImage(config imageRenderConfig) ([]types.Triangle, error) {
fmt.Println(closeErr)
}
}()

img, err := png.Decode(reader)
if err != nil {
return nil, errors.New(errors.IOError, "failed to decode PNG", err)
}

// Get image size
bounds := img.Bounds()
width := bounds.Max.X
height := bounds.Max.Y

scale := config.height / float64(height)
logoWidth := bounds.Max.X
logoHeight := bounds.Max.Y

// Transfer image pixels onto face of skyline as voxels
var triangles []types.Triangle

for y := height - 1; y >= 0; y-- {
for x := 0; x < width; x++ {
for x := 0; x < logoWidth; x++ {
for y := logoHeight - 1; y >= 0; y-- {
// Get pixel color and alpha
r, _, _, a := img.At(x, y).RGBA()

// If pixel is active (white) and not fully transparent, create a voxel
if a > 32768 && r > 32768 {
xPos := config.startX + float64(x)*config.voxelScale*scale
zPos := config.startZ + float64(height-1-y)*config.voxelScale*scale

voxel, err := CreateCube(
xPos,
config.startY,
zPos,
config.voxelScale*scale,
config.depth,
config.voxelScale*scale,

voxel, err := createVoxelOnFace(
(leftOffsetPercent*float64(faceWidthRes))+float64(x)*scale,
(topOffsetPercent*float64(faceHeightRes))+float64(y)*scale,
height,
baseWidth,
baseHeight,
)

if err != nil {
Expand Down
Loading
Loading